From 18bfb9419553450d681e5e8ac8fff04173b645c3 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Fri, 23 Oct 2026 17:02:32 -0400 Subject: [PATCH 1/7] feat(clerk-js): support Slack workspace hints during authentication --- .changeset/bright-slack-workspaces.md | 6 + .../clerk-js/src/core/resources/SignIn.ts | 56 ++- .../clerk-js/src/core/resources/SignUp.ts | 4 + .../core/resources/__tests__/SignIn.test.ts | 458 +++++++++++++++++- .../core/resources/__tests__/SignUp.test.ts | 184 ++++++- packages/shared/src/types/factors.ts | 4 + packages/shared/src/types/redirects.ts | 4 + packages/shared/src/types/signInCommon.ts | 4 + packages/shared/src/types/signInFuture.ts | 8 + packages/shared/src/types/signUpCommon.ts | 8 + packages/shared/src/types/signUpFuture.ts | 4 + 11 files changed, 718 insertions(+), 22 deletions(-) create mode 100644 .changeset/bright-slack-workspaces.md diff --git a/.changeset/bright-slack-workspaces.md b/.changeset/bright-slack-workspaces.md new file mode 100644 index 00000000000..f848ba382d3 --- /dev/null +++ b/.changeset/bright-slack-workspaces.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add the optional `slackTeamId` workspace hint to Slack sign-in and sign-up attempts, including redirect, popup, and unified SSO flows. diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 24c325edbf1..f31cf0bd72f 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -303,7 +303,11 @@ export class SignIn extends BaseResource implements SignInResource { clerkInvalidStrategy('SignIn.prepareFirstFactor', params.strategy); } return this._basePost({ - body: { ...config, strategy: params.strategy }, + body: { + ...config, + strategy: params.strategy, + slackTeamId: 'slackTeamId' in params ? params.slackTeamId : undefined, + }, action: 'prepare_first_factor', coalesce: true, }); @@ -423,8 +427,15 @@ export class SignIn extends BaseResource implements SignInResource { params: AuthenticateWithRedirectParams, navigateCallback: (url: URL | string) => void, ): Promise => { - const { strategy, redirectUrlComplete, identifier, oidcPrompt, continueSignIn, enterpriseConnectionId } = - params || {}; + const { + strategy, + redirectUrlComplete, + identifier, + oidcPrompt, + slackTeamId, + continueSignIn, + enterpriseConnectionId, + } = params || {}; const actionCompleteRedirectUrl = redirectUrlComplete; const redirectUrl = SignIn.clerk.buildUrlWithAuth(params.redirectUrl); @@ -450,13 +461,15 @@ export class SignIn extends BaseResource implements SignInResource { // provider first" and the challenge runs on the way back, where the callback routes to it. let challengedCreateHandOff: URL | null = null; - if (!this.id || !continueSignIn) { + const shouldCreateSignIn = !this.id || !continueSignIn; + if (shouldCreateSignIn) { await this.create({ strategy, identifier, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, + slackTeamId, }); if (isChallengePending()) { @@ -467,14 +480,16 @@ export class SignIn extends BaseResource implements SignInResource { } } - if (strategy === 'enterprise_sso') { - await this.prepareFirstFactor({ + if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamId !== undefined)) { + const prepareParams = { strategy, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, + slackTeamId, enterpriseConnectionId, - }); + }; + await this._basePost({ body: prepareParams, action: 'prepare_first_factor', coalesce: true }); // A challenged prepare builds no verification, so any redirect left on the sign-in is from an // earlier attempt and may be for another connection. Only this call's create hand-off is safe @@ -1366,8 +1381,16 @@ class SignInFuture implements SignInFutureResource { } async sso(params: SignInFutureSSOParams): Promise<{ error: ClerkError | null }> { - const { strategy, redirectUrl, redirectCallbackUrl, popup, oidcPrompt, enterpriseConnectionId, identifier } = - params; + const { + strategy, + redirectUrl, + redirectCallbackUrl, + popup, + oidcPrompt, + slackTeamId, + enterpriseConnectionId, + identifier, + } = params; return runAsyncResourceTask(this.#resource, async () => { let actionCompleteRedirectUrl = redirectUrl; try { @@ -1386,15 +1409,6 @@ class SignInFuture implements SignInFutureResource { routes.actionCompleteRedirectUrl = wrappedRoutes.redirectUrl; } - // Reuse the existing sign-in by default so any state already attached to it carries - // into the SSO attempt (e.g. a ticket from `signIn.create({ strategy: 'ticket' })`, - // or a discovered identifier). OAuth is the exception: its redirect URL only comes - // back from `_create` and cannot be refreshed in place, so any redirect already - // lingering on the resource is stale. Reusing it would replay a previous attempt's - // redirect, including a retry of the same provider after an abandoned redirect - // (SDK-75), so whenever an OAuth call finds a pending redirect we start fresh. - // `enterprise_sso` is always safe to reuse because the `prepare_first_factor` call - // below refreshes its redirect against the existing sign-in. const hasPendingRedirect = !!this.#resource.firstFactorVerification.externalVerificationRedirectURL; const wouldReplayStaleRedirect = strategy !== 'enterprise_sso' && hasPendingRedirect; const shouldCreateSignIn = !this.#resource.id || wouldReplayStaleRedirect; @@ -1415,6 +1429,7 @@ class SignInFuture implements SignInFutureResource { ...routes, identifier, oidcPrompt, + slackTeamId, }, { resolveProtectCheck: false }, ); @@ -1427,15 +1442,16 @@ class SignInFuture implements SignInFutureResource { } } - if (strategy === 'enterprise_sso') { + if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamId !== undefined)) { const prepare = (options?: { resolveProtectCheck?: boolean }) => this.#resource.__internal_basePost( { body: { ...routes, oidcPrompt, + slackTeamId, enterpriseConnectionId, - strategy: 'enterprise_sso', + strategy, }, action: 'prepare_first_factor', coalesce: true, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 1babb4b914c..1f3f8fdfe6c 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -452,6 +452,7 @@ export class SignUp extends BaseResource implements SignUpResource { emailAddress, legalAccepted, oidcPrompt, + slackTeamId, enterpriseConnectionId, } = params; @@ -466,6 +467,7 @@ export class SignUp extends BaseResource implements SignUpResource { emailAddress, legalAccepted, oidcPrompt, + slackTeamId, enterpriseConnectionId, }; return continueSignUp && this.id ? this.update(authParams) : this.create(authParams); @@ -1099,6 +1101,7 @@ class SignUpFuture implements SignUpFutureResource { unsafeMetadata, legalAccepted, oidcPrompt, + slackTeamId, enterpriseConnectionId, emailAddress, popup, @@ -1134,6 +1137,7 @@ class SignUpFuture implements SignUpFutureResource { unsafeMetadata, legalAccepted, oidcPrompt, + slackTeamId, enterpriseConnectionId, emailAddress, captchaToken, diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 2019cf9e5f3..f1156af31d3 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -3,12 +3,16 @@ import { camelToSnake } from '@clerk/shared/underscore'; import { createDeferredPromise } from '@clerk/shared/utils'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Clerk } from '../../clerk'; import { eventBus } from '../../events'; +import { createFapiClient } from '../../fapiClient'; import { ProtectCheckGate } from '../../protectCheckGate'; import { signInErrorSignal, signInResourceSignal } from '../../signals'; -import { BaseResource } from '../internal'; +import { BaseResource, Client, Environment } from '../internal'; import { SignIn } from '../SignIn'; +const resourceFetch = BaseResource._fetch; + // Mock the authenticateWithPopup module vi.mock('../../../utils/authenticateWithPopup', async () => { const actual = await vi.importActual('../../../utils/authenticateWithPopup'); @@ -4333,3 +4337,455 @@ describe('SignIn protect_check gate', () => { }); }); }); + +describe('SignIn Slack workspace hint requests', () => { + const fetchMock = vi.fn(); + const response = { + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor' as const, + first_factor_verification: { + ...new SignIn().__internal_toSnapshot().first_factor_verification, + status: 'unverified' as const, + external_verification_redirect_url: 'https://slack.com/openid/connect/authorize', + }, + }; + const jsonResponse = (payload: unknown, status = 200) => ({ + ok: status === 200, + status, + headers: new Headers(), + json: () => Promise.resolve(payload), + }); + const workspaceResponse = (team: string) => ({ + ...response, + first_factor_verification: { + ...response.first_factor_verification, + status: 'unverified' as const, + external_verification_redirect_url: `https://slack.com/openid/connect/authorize?team=${team}`, + }, + }); + const challengedResponse = { + ...workspaceResponse('TOLD'), + status: 'needs_protect_check', + protect_check: { status: 'pending', token: 'challenge', sdk_url: 'https://protect.example/sdk.js' }, + }; + let previousFetch: typeof BaseResource._fetch; + let previousBaseClerk: typeof BaseResource.clerk; + let previousClerk: typeof SignIn.clerk; + const fields = (index = 0) => new URLSearchParams(fetchMock.mock.calls[index][1].body); + + beforeEach(() => { + previousFetch = BaseResource._fetch; + previousBaseClerk = BaseResource.clerk; + previousClerk = SignIn.clerk; + BaseResource._fetch = resourceFetch; + fetchMock.mockReset().mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ client: null, response }), + }); + vi.stubGlobal('fetch', fetchMock); + const fapiClient = createFapiClient({ + frontendApi: 'clerk.example.com', + instanceType: 'production', + getSessionId: () => undefined, + }); + const clerk = new Clerk('pk_live_Y2xlcmsuZXhhbXBsZS5jb20k'); + clerk.getFapiClient = () => fapiClient; + clerk.__internal_setCountry = vi.fn(); + clerk.buildUrlWithAuth = url => url; + clerk.__internal_windowNavigate = vi.fn(); + clerk.client = Client.getOrCreateInstance(); + const environment = new Environment(); + vi.spyOn(clerk, '__internal_environment', 'get').mockReturnValue(environment); + BaseResource.clerk = SignIn.clerk = clerk; + vi.spyOn(ProtectCheckGate.prototype, 'resolve').mockResolvedValue(undefined); + }); + + afterEach(() => { + BaseResource._fetch = previousFetch; + BaseResource.clerk = previousBaseClerk; + SignIn.clerk = previousClerk; + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it.each(['T123ABC', '', undefined])('serializes a low-level create hint of %s', async slackTeamId => { + await new SignIn().create({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', slackTeamId }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fields().get('slack_team_id')).toBe(slackTeamId ?? null); + expect(fields().has('slackTeamId')).toBe(false); + }); + + it('serializes a unified low-level create hint', async () => { + const { error } = await new SignIn().__internal_future.create({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + }); + expect(error).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + }); + + it.each(['redirect', 'popup'] as const)('forwards a hint through legacy %s authentication', async mode => { + const signIn = new SignIn(); + const params = { + strategy: 'oauth_slack' as const, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + slackTeamId: 'T123ABC', + }; + if (mode === 'popup') { + await signIn.authenticateWithPopup({ ...params, popup: { location: { href: '' } } as Window }); + } else { + await signIn.authenticateWithRedirect(params); + } + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + }); + + it.each(['redirect', 'popup'] as const)('refreshes the workspace on legacy %s continuation', async mode => { + const signIn = new SignIn(); + const popup = { location: { href: '' } } as Window; + const params = { + strategy: 'oauth_slack' as const, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + }; + const authenticate = (slackTeamId: string, continueSignIn = false) => + mode === 'popup' + ? signIn.authenticateWithPopup({ ...params, slackTeamId, continueSignIn, popup }) + : signIn.authenticateWithRedirect({ ...params, slackTeamId, continueSignIn }); + + fetchMock + .mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T123ABC') })) + .mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T456DEF') })); + await authenticate('T123ABC'); + await authenticate('T456DEF', true); + + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual([ + '/v1/client/sign_ins', + '/v1/client/sign_ins/signin_slack/prepare_first_factor', + ]); + expect([fields(0).get('slack_team_id'), fields(1).get('slack_team_id')]).toEqual(['T123ABC', 'T456DEF']); + if (mode === 'popup') { + expect(popup.location.href).toBe('https://slack.com/openid/connect/authorize?team=T456DEF'); + } else { + expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenLastCalledWith( + new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), + ); + } + }); + + it.each([ + { strategy: 'oauth_slack' as const, slackTeamId: '' }, + { strategy: 'oauth_google' as const, slackTeamId: 'T123ABC' }, + ])('forwards a rejected legacy continuation hint for $strategy with "$slackTeamId"', async params => { + fetchMock.mockResolvedValueOnce(jsonResponse({ errors: [{ code: 'form_param_value_invalid' }] }, 422)); + const signIn = new SignIn(workspaceResponse('TOLD')); + + await expect( + signIn.authenticateWithRedirect({ + ...params, + continueSignIn: true, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + }), + ).rejects.toMatchObject({ errors: [{ code: 'form_param_value_invalid' }] }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); + expect(fields().get('strategy')).toBe(params.strategy); + expect(fields().get('slack_team_id')).toBe(params.slackTeamId); + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + }); + + it('reuses the legacy redirect without a request when continuation omits the hint', async () => { + const signIn = new SignIn(workspaceResponse('T123ABC')); + await signIn.authenticateWithRedirect({ + strategy: 'oauth_slack', + continueSignIn: true, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + }); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledWith( + new URL('https://slack.com/openid/connect/authorize?team=T123ABC'), + ); + }); + + it('continues the same legacy attempt after Protect without following its stale redirect', async () => { + const signIn = new SignIn(workspaceResponse('TOLD')); + fetchMock + .mockResolvedValueOnce(jsonResponse({ response: challengedResponse })) + .mockResolvedValueOnce(jsonResponse({ response: { id: 'signin_slack', status: 'needs_first_factor' } })) + .mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T456DEF') })); + const params = { + strategy: 'oauth_slack' as const, + continueSignIn: true, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + slackTeamId: 'T456DEF', + }; + + await expect(signIn.authenticateWithRedirect(params)).rejects.toMatchObject({ code: 'protect_check_required' }); + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + await signIn.submitProtectCheck({ proofToken: 'proof' }); + await signIn.authenticateWithRedirect(params); + + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual([ + '/v1/client/sign_ins/signin_slack/prepare_first_factor', + '/v1/client/sign_ins/signin_slack/protect_check', + '/v1/client/sign_ins/signin_slack/prepare_first_factor', + ]); + expect(fields(0).get('slack_team_id')).toBe('T456DEF'); + expect(fields(2).get('slack_team_id')).toBe('T456DEF'); + expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledExactlyOnceWith( + new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), + ); + }); + + it.each(['redirect', 'popup'] as const)('prepares a workspace on an existing unified %s attempt', async mode => { + const signIn = new SignIn({ + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor', + }); + const popup = mode === 'popup' ? ({ location: { href: '' } } as Window) : undefined; + vi.mocked(_futureAuthenticateWithPopup).mockImplementation((_clerk, params) => { + params.popup.location.href = params.externalVerificationRedirectURL.toString(); + return Promise.resolve(); + }); + fetchMock.mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T456DEF') })); + const { error } = await signIn.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T456DEF', + popup, + }); + + expect(error).toBeNull(); + expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); + expect(fields().get('slack_team_id')).toBe('T456DEF'); + if (popup) { + expect(popup.location.href).toBe('https://slack.com/openid/connect/authorize?team=T456DEF'); + } else { + expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledWith( + new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), + ); + } + }); + + it.each([ + { strategy: 'oauth_slack' as const, slackTeamId: '' }, + { strategy: 'oauth_google' as const, slackTeamId: 'T123ABC' }, + ])('forwards a rejected existing unified hint for $strategy with "$slackTeamId"', async params => { + fetchMock.mockResolvedValueOnce(jsonResponse({ errors: [{ code: 'form_param_value_invalid' }] }, 422)); + const signIn = new SignIn({ + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor', + }); + const { error } = await signIn.__internal_future.sso({ + ...params, + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + }); + + expect(error).toMatchObject({ errors: [{ code: 'form_param_value_invalid' }] }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); + expect(fields().get('strategy')).toBe(params.strategy); + expect(fields().get('slack_team_id')).toBe(params.slackTeamId); + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + }); + + it('keeps existing unified reuse without a request when the hint is omitted', async () => { + const signIn = new SignIn({ + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor', + }); + const { error } = await signIn.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + }); + + expect(error).toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + }); + + it('retries unified workspace preparation on the same attempt after Protect', async () => { + const signIn = new SignIn({ + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor', + }); + fetchMock + .mockResolvedValueOnce(jsonResponse({ response: challengedResponse })) + .mockResolvedValueOnce(jsonResponse({ response: { id: 'signin_slack', status: 'needs_first_factor' } })) + .mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T456DEF') })); + vi.mocked(ProtectCheckGate.prototype.resolve).mockImplementation(async () => { + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + if (signIn.protectCheck) { + await signIn.submitProtectCheck({ proofToken: 'proof' }); + } + }); + const { error } = await signIn.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T456DEF', + oidcPrompt: 'consent', + }); + + expect(error).toBeNull(); + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual([ + '/v1/client/sign_ins/signin_slack/prepare_first_factor', + '/v1/client/sign_ins/signin_slack/protect_check', + '/v1/client/sign_ins/signin_slack/prepare_first_factor', + ]); + expect(fields(0).get('slack_team_id')).toBe('T456DEF'); + expect(fields(2)).toEqual(fields(0)); + expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledExactlyOnceWith( + new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), + ); + }); + + it('does not follow a stale unified redirect when Protect resolution fails', async () => { + const signIn = new SignIn({ + ...new SignIn().__internal_toSnapshot(), + id: 'signin_slack', + status: 'needs_first_factor', + }); + fetchMock.mockResolvedValueOnce(jsonResponse({ response: challengedResponse })); + vi.mocked(ProtectCheckGate.prototype.resolve).mockRejectedValueOnce(new Error('challenge failed')); + const { error } = await signIn.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T456DEF', + }); + + expect(error).toMatchObject({ message: 'challenge failed' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); + }); + + it('forwards incompatible legacy enterprise hints to first-factor preparation', async () => { + const signIn = new SignIn(); + signIn.id = 'signin_existing'; + await signIn.authenticateWithRedirect({ + strategy: 'enterprise_sso', + continueSignIn: true, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + slackTeamId: 'T123ABC', + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_existing/prepare_first_factor'); + expect(fields().get('strategy')).toBe('enterprise_sso'); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + }); + + it('preserves unsupported legacy social first-factor preparation', () => { + const signIn = new SignIn(); + signIn.id = 'signin_existing'; + expect(() => + signIn.prepareFirstFactor({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/callback', + actionCompleteRedirectUrl: 'https://example.com/dashboard', + slackTeamId: 'T123ABC', + }), + ).toThrow(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('creates fresh unified attempts for changed workspaces and then omits an absent hint', async () => { + const signIn = new SignIn(); + for (const slackTeamId of ['T123ABC', 'T456DEF', undefined]) { + const { error } = await signIn.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId, + }); + expect(error).toBeNull(); + } + expect(fetchMock).toHaveBeenCalledTimes(3); + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual(Array(3).fill('/v1/client/sign_ins')); + expect([0, 1, 2].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', null]); + }); + + it('forwards a unified popup hint to the create request', async () => { + const popup = { location: { href: '' } } as Window; + vi.mocked(_futureAuthenticateWithPopup).mockResolvedValue(undefined); + const { error } = await new SignIn().__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + popup, + }); + expect(error).toBeNull(); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(_futureAuthenticateWithPopup).toHaveBeenCalledWith(SignIn.clerk, expect.objectContaining({ popup })); + }); + + it.each(['oauth_google', 'enterprise_sso'] as const)('forwards incompatible unified %s hints', async strategy => { + const { error } = await new SignIn().__internal_future.sso({ + strategy, + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + }); + expect(error).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(strategy === 'enterprise_sso' ? 2 : 1); + for (let index = 0; index < fetchMock.mock.calls.length; index++) { + expect(fields(index).get('strategy')).toBe(strategy); + expect(fields(index).get('slack_team_id')).toBe('T123ABC'); + } + }); + + it('preserves the hint when unified enterprise preparation retries after a challenge', async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + headers: new Headers(), + json: () => + Promise.resolve({ + response: { + id: 'signin_existing', + status: 'needs_protect_check', + protect_check: { status: 'pending', token: 'challenge', sdk_url: 'https://protect.example/sdk.js' }, + }, + }), + }); + const signIn = new SignIn(); + signIn.id = 'signin_existing'; + vi.mocked(ProtectCheckGate.prototype.resolve).mockImplementation(() => { + signIn.protectCheck = null; + return Promise.resolve(); + }); + const { error } = await signIn.__internal_future.sso({ + strategy: 'enterprise_sso', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + }); + expect(error).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fields(0).get('slack_team_id')).toBe('T123ABC'); + expect(fields(1).get('slack_team_id')).toBe('T123ABC'); + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual( + Array(2).fill('/v1/client/sign_ins/signin_existing/prepare_first_factor'), + ); + }); +}); diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index b5fdc758c6b..5b56b7b5454 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -1,12 +1,16 @@ import { createDeferredPromise } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Clerk } from '../../clerk'; import { eventBus } from '../../events'; +import { createFapiClient } from '../../fapiClient'; import { ProtectCheckGate } from '../../protectCheckGate'; import { signUpErrorSignal, signUpResourceSignal } from '../../signals'; -import { BaseResource } from '../internal'; +import { BaseResource, Client, Environment } from '../internal'; import { SignUp } from '../SignUp'; +const resourceFetch = BaseResource._fetch; + // Mock the authenticateWithPopup module vi.mock('../../../utils/authenticateWithPopup', async () => { const actual = await vi.importActual('../../../utils/authenticateWithPopup'); @@ -2478,3 +2482,181 @@ describe('SignUp protect_check gate', () => { }); }); }); + +describe('SignUp Slack workspace hint requests', () => { + const fetchMock = vi.fn(); + const response = { + id: 'signup_slack', + status: 'missing_requirements', + verifications: { + external_account: { + status: 'unverified', + external_verification_redirect_url: 'https://slack.com/openid/connect/authorize', + }, + }, + }; + let previousFetch: typeof BaseResource._fetch; + let previousBaseClerk: typeof BaseResource.clerk; + let previousClerk: typeof SignUp.clerk; + const fields = (index = 0) => new URLSearchParams(fetchMock.mock.calls[index][1].body); + + beforeEach(() => { + previousFetch = BaseResource._fetch; + previousBaseClerk = BaseResource.clerk; + previousClerk = SignUp.clerk; + BaseResource._fetch = resourceFetch; + fetchMock.mockReset().mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ client: null, response }), + }); + vi.stubGlobal('fetch', fetchMock); + const fapiClient = createFapiClient({ + frontendApi: 'clerk.example.com', + instanceType: 'production', + getSessionId: () => undefined, + }); + const clerk = new Clerk('pk_live_Y2xlcmsuZXhhbXBsZS5jb20k'); + clerk.getFapiClient = () => fapiClient; + clerk.__internal_setCountry = vi.fn(); + clerk.buildUrlWithAuth = url => url; + clerk.__internal_windowNavigate = vi.fn(); + clerk.client = Client.getOrCreateInstance(); + const environment = new Environment(); + vi.spyOn(clerk, '__internal_environment', 'get').mockReturnValue(environment); + vi.spyOn(environment, 'reload').mockResolvedValue(environment); + BaseResource.clerk = SignUp.clerk = clerk; + vi.spyOn(ProtectCheckGate.prototype, 'resolve').mockResolvedValue(undefined); + }); + + afterEach(() => { + BaseResource._fetch = previousFetch; + BaseResource.clerk = previousBaseClerk; + SignUp.clerk = previousClerk; + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it.each(['create', 'update', 'prepareVerification'] as const)( + 'serializes per-attempt hints through low-level %s', + async method => { + const signUp = new SignUp(); + if (method !== 'create') { + signUp.id = 'signup_slack'; + } + for (const slackTeamId of ['T123ABC', 'T456DEF', '', undefined]) { + await signUp[method]({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', slackTeamId }); + } + expect(fetchMock).toHaveBeenCalledTimes(4); + expect([0, 1, 2, 3].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', '', null]); + expect(fetchMock.mock.calls.every((_, index) => !fields(index).has('slackTeamId'))).toBe(true); + const expectedPath = + method === 'create' + ? '/v1/client/sign_ups' + : method === 'update' + ? '/v1/client/sign_ups/signup_slack' + : '/v1/client/sign_ups/signup_slack/prepare_verification'; + expect(fetchMock.mock.calls.every(([url]) => url.pathname === expectedPath)).toBe(true); + }, + ); + + it.each([ + { popup: false, continueSignUp: false }, + { popup: true, continueSignUp: false }, + { popup: false, continueSignUp: true }, + { popup: true, continueSignUp: true }, + ])('forwards a legacy authentication hint with %j', async ({ popup, continueSignUp }) => { + const signUp = new SignUp(); + if (continueSignUp) { + signUp.id = 'signup_existing'; + } + const params = { + strategy: 'oauth_slack' as const, + redirectUrl: 'https://example.com/callback', + redirectUrlComplete: 'https://example.com/dashboard', + slackTeamId: 'T123ABC', + continueSignUp, + }; + if (popup) { + await signUp.authenticateWithPopup({ ...params, popup: { location: { href: '' } } as Window }); + } else { + await signUp.authenticateWithRedirect(params); + } + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fetchMock.mock.calls[0][0].pathname).toBe( + continueSignUp ? '/v1/client/sign_ups/signup_existing' : '/v1/client/sign_ups', + ); + expect(fetchMock.mock.calls[0][0].searchParams.get('_method')).toBe(continueSignUp ? 'PATCH' : null); + }); + + it('forwards unified create and update hints without retaining the previous workspace', async () => { + const signUp = new SignUp(); + for (const slackTeamId of ['T123ABC', 'T456DEF', undefined]) { + const { error } = await signUp.__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId, + }); + expect(error).toBeNull(); + } + expect(fetchMock).toHaveBeenCalledTimes(3); + expect([0, 1, 2].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', null]); + expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual([ + '/v1/client/sign_ups', + '/v1/client/sign_ups/signup_slack', + '/v1/client/sign_ups/signup_slack', + ]); + expect(fetchMock.mock.calls.map(([url]) => url.searchParams.get('_method'))).toEqual([null, 'PATCH', 'PATCH']); + }); + + it('forwards a unified popup hint to the create request', async () => { + const popup = { location: { href: '' } } as Window; + vi.mocked(_futureAuthenticateWithPopup).mockResolvedValue(undefined); + const { error } = await new SignUp().__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + popup, + }); + expect(error).toBeNull(); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(_futureAuthenticateWithPopup).toHaveBeenCalledWith(SignUp.clerk, expect.objectContaining({ popup })); + }); + + it.each(['oauth_google', 'enterprise_sso'])('forwards incompatible unified %s hints', async strategy => { + const { error } = await new SignUp().__internal_future.sso({ + strategy, + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + }); + expect(error).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fields().get('strategy')).toBe(strategy); + expect(fields().get('slack_team_id')).toBe('T123ABC'); + }); + + it('preserves a unified hint when an environment change requires a retry', async () => { + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 400, + headers: new Headers(), + json: () => Promise.resolve({ errors: [{ code: 'captcha_invalid', message: 'Invalid captcha' }] }), + }); + const { error } = await new SignUp().__internal_future.sso({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/dashboard', + redirectCallbackUrl: 'https://example.com/callback', + slackTeamId: 'T123ABC', + }); + expect(error).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fields(0).get('slack_team_id')).toBe('T123ABC'); + expect(fields(1).get('slack_team_id')).toBe('T123ABC'); + expect(SignUp.clerk.__internal_environment?.reload).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/shared/src/types/factors.ts b/packages/shared/src/types/factors.ts index 4531ebf7249..9b22e6cf525 100644 --- a/packages/shared/src/types/factors.ts +++ b/packages/shared/src/types/factors.ts @@ -239,6 +239,10 @@ export type OAuthConfig = OauthFactor & { * The OIDC login hint parameter to use for the OAuth flow. */ oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; }; /** @generateWithEmptyComment */ diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index 2fb281c9b90..7e7b3259fed 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -76,6 +76,10 @@ export type AuthenticateWithRedirectParams = { * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; /** * Internal OAuth callback context, consumed only when an OAuth transport completes the diff --git a/packages/shared/src/types/signInCommon.ts b/packages/shared/src/types/signInCommon.ts index 46c8f8133ab..a90cf4f3887 100644 --- a/packages/shared/src/types/signInCommon.ts +++ b/packages/shared/src/types/signInCommon.ts @@ -145,6 +145,10 @@ export type SignInCreateParams = ( identifier?: string; oidcPrompt?: string; oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; } | { strategy: TicketStrategy; diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 65c2d6354d5..09f05bc83b5 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -35,6 +35,10 @@ export interface SignInFutureCreateParams { * The value to pass to the [OIDC `login_hint` parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=login_hint,in%20\(if%20necessary\).) in the generated OAuth redirect URL. */ oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; /** * The URL that the user will be redirected to, after successful authorization from the OAuth provider and Clerk sign-in. */ @@ -243,6 +247,10 @@ export interface SignInFutureSSOParams { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental diff --git a/packages/shared/src/types/signUpCommon.ts b/packages/shared/src/types/signUpCommon.ts index da860fe5533..74cc0741277 100644 --- a/packages/shared/src/types/signUpCommon.ts +++ b/packages/shared/src/types/signUpCommon.ts @@ -79,6 +79,10 @@ export type PrepareVerificationParams = actionCompleteRedirectUrl?: string; oidcPrompt?: string; oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; } | { strategy: EnterpriseSSOStrategy; @@ -134,6 +138,10 @@ export type SignUpCreateParams = Partial< legalAccepted: boolean; oidcPrompt: string; oidcLoginHint: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; channel: PhoneCodeChannel; locale?: string; /** diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index 773a94043c2..69de5c02711 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -248,6 +248,10 @@ export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamId?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental From 5de08942805cd7cd9959cdf2d8ac1655f5b99f2e Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Sun, 25 Oct 2026 19:43:23 -0400 Subject: [PATCH 2/7] fix(clerk-js): remove unsupported Slack preparation option --- packages/clerk-js/src/core/resources/SignIn.ts | 6 +----- .../clerk-js/src/core/resources/__tests__/SignIn.test.ts | 1 - packages/shared/src/types/factors.ts | 4 ---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index f31cf0bd72f..c9a25a3e522 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -303,11 +303,7 @@ export class SignIn extends BaseResource implements SignInResource { clerkInvalidStrategy('SignIn.prepareFirstFactor', params.strategy); } return this._basePost({ - body: { - ...config, - strategy: params.strategy, - slackTeamId: 'slackTeamId' in params ? params.slackTeamId : undefined, - }, + body: { ...config, strategy: params.strategy }, action: 'prepare_first_factor', coalesce: true, }); diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index f1156af31d3..c220fd3cb98 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -4702,7 +4702,6 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', actionCompleteRedirectUrl: 'https://example.com/dashboard', - slackTeamId: 'T123ABC', }), ).toThrow(); expect(fetchMock).not.toHaveBeenCalled(); diff --git a/packages/shared/src/types/factors.ts b/packages/shared/src/types/factors.ts index 9b22e6cf525..4531ebf7249 100644 --- a/packages/shared/src/types/factors.ts +++ b/packages/shared/src/types/factors.ts @@ -239,10 +239,6 @@ export type OAuthConfig = OauthFactor & { * The OIDC login hint parameter to use for the OAuth flow. */ oidcLoginHint?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamId?: string; }; /** @generateWithEmptyComment */ From 449c40b4df1ebb5ed0f102cea91ebd339e59a6d7 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Sun, 25 Oct 2026 19:53:07 -0400 Subject: [PATCH 3/7] docs(clerk-js): clarify sign-in reuse and Slack hint preparation --- packages/clerk-js/src/core/resources/SignIn.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index c9a25a3e522..ab9c6fdd06f 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -1405,6 +1405,14 @@ class SignInFuture implements SignInFutureResource { routes.actionCompleteRedirectUrl = wrappedRoutes.redirectUrl; } + // Reuse the existing sign-in by default so any state already attached to it carries + // into the SSO attempt (e.g. a ticket from `signIn.create({ strategy: 'ticket' })`, + // or a discovered identifier). For OAuth, a pending redirect may belong to an + // abandoned attempt, including a retry of the same provider (SDK-75), so start + // fresh rather than replay it. `enterprise_sso` can reuse the sign-in because + // `prepare_first_factor` below refreshes its redirect. When reusing a sign-in + // without a pending redirect, an explicit `slackTeamId` also triggers preparation + // so the hint is applied to the existing attempt. const hasPendingRedirect = !!this.#resource.firstFactorVerification.externalVerificationRedirectURL; const wouldReplayStaleRedirect = strategy !== 'enterprise_sso' && hasPendingRedirect; const shouldCreateSignIn = !this.#resource.id || wouldReplayStaleRedirect; From 51aa7403d80d1378fea620fdfb57e3783c0a1dd0 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 27 Oct 2026 10:22:40 -0400 Subject: [PATCH 4/7] refactor(clerk-js): rename Slack workspace hint parameter --- .changeset/bright-slack-workspaces.md | 2 +- .../clerk-js/src/core/resources/SignIn.ts | 18 ++-- .../clerk-js/src/core/resources/SignUp.ts | 8 +- .../core/resources/__tests__/SignIn.test.ts | 84 ++++++++++--------- .../core/resources/__tests__/SignUp.test.ts | 37 ++++---- packages/shared/src/types/redirects.ts | 2 +- packages/shared/src/types/signInCommon.ts | 2 +- packages/shared/src/types/signInFuture.ts | 4 +- packages/shared/src/types/signUpCommon.ts | 4 +- packages/shared/src/types/signUpFuture.ts | 2 +- 10 files changed, 86 insertions(+), 77 deletions(-) diff --git a/.changeset/bright-slack-workspaces.md b/.changeset/bright-slack-workspaces.md index f848ba382d3..550a972de4c 100644 --- a/.changeset/bright-slack-workspaces.md +++ b/.changeset/bright-slack-workspaces.md @@ -3,4 +3,4 @@ '@clerk/shared': minor --- -Add the optional `slackTeamId` workspace hint to Slack sign-in and sign-up attempts, including redirect, popup, and unified SSO flows. +Add the optional `slackTeamIdHint` workspace hint to Slack sign-in and sign-up attempts, including redirect, popup, and unified SSO flows. diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index ab9c6fdd06f..83caee52941 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -428,7 +428,7 @@ export class SignIn extends BaseResource implements SignInResource { redirectUrlComplete, identifier, oidcPrompt, - slackTeamId, + slackTeamIdHint, continueSignIn, enterpriseConnectionId, } = params || {}; @@ -465,7 +465,7 @@ export class SignIn extends BaseResource implements SignInResource { redirectUrl, actionCompleteRedirectUrl, oidcPrompt, - slackTeamId, + slackTeamIdHint, }); if (isChallengePending()) { @@ -476,13 +476,13 @@ export class SignIn extends BaseResource implements SignInResource { } } - if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamId !== undefined)) { + if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamIdHint !== undefined)) { const prepareParams = { strategy, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, }; await this._basePost({ body: prepareParams, action: 'prepare_first_factor', coalesce: true }); @@ -1383,7 +1383,7 @@ class SignInFuture implements SignInFutureResource { redirectCallbackUrl, popup, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, identifier, } = params; @@ -1411,7 +1411,7 @@ class SignInFuture implements SignInFutureResource { // abandoned attempt, including a retry of the same provider (SDK-75), so start // fresh rather than replay it. `enterprise_sso` can reuse the sign-in because // `prepare_first_factor` below refreshes its redirect. When reusing a sign-in - // without a pending redirect, an explicit `slackTeamId` also triggers preparation + // without a pending redirect, an explicit `slackTeamIdHint` also triggers preparation // so the hint is applied to the existing attempt. const hasPendingRedirect = !!this.#resource.firstFactorVerification.externalVerificationRedirectURL; const wouldReplayStaleRedirect = strategy !== 'enterprise_sso' && hasPendingRedirect; @@ -1433,7 +1433,7 @@ class SignInFuture implements SignInFutureResource { ...routes, identifier, oidcPrompt, - slackTeamId, + slackTeamIdHint, }, { resolveProtectCheck: false }, ); @@ -1446,14 +1446,14 @@ class SignInFuture implements SignInFutureResource { } } - if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamId !== undefined)) { + if (strategy === 'enterprise_sso' || (!shouldCreateSignIn && slackTeamIdHint !== undefined)) { const prepare = (options?: { resolveProtectCheck?: boolean }) => this.#resource.__internal_basePost( { body: { ...routes, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, strategy, }, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 1f3f8fdfe6c..b335d7cfe14 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -452,7 +452,7 @@ export class SignUp extends BaseResource implements SignUpResource { emailAddress, legalAccepted, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, } = params; @@ -467,7 +467,7 @@ export class SignUp extends BaseResource implements SignUpResource { emailAddress, legalAccepted, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, }; return continueSignUp && this.id ? this.update(authParams) : this.create(authParams); @@ -1101,7 +1101,7 @@ class SignUpFuture implements SignUpFutureResource { unsafeMetadata, legalAccepted, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, emailAddress, popup, @@ -1137,7 +1137,7 @@ class SignUpFuture implements SignUpFutureResource { unsafeMetadata, legalAccepted, oidcPrompt, - slackTeamId, + slackTeamIdHint, enterpriseConnectionId, emailAddress, captchaToken, diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index c220fd3cb98..53a50f0baa0 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -4411,22 +4411,26 @@ describe('SignIn Slack workspace hint requests', () => { vi.unstubAllGlobals(); }); - it.each(['T123ABC', '', undefined])('serializes a low-level create hint of %s', async slackTeamId => { - await new SignIn().create({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', slackTeamId }); + it.each(['T123ABC', '', undefined])('serializes a low-level create hint of %s', async slackTeamIdHint => { + await new SignIn().create({ + strategy: 'oauth_slack', + redirectUrl: 'https://example.com/callback', + slackTeamIdHint, + }); expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fields().get('slack_team_id')).toBe(slackTeamId ?? null); - expect(fields().has('slackTeamId')).toBe(false); + expect(fields().get('slack_team_id_hint')).toBe(slackTeamIdHint ?? null); + expect(fields().has('slackTeamIdHint')).toBe(false); }); it('serializes a unified low-level create hint', async () => { const { error } = await new SignIn().__internal_future.create({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(error).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); }); it.each(['redirect', 'popup'] as const)('forwards a hint through legacy %s authentication', async mode => { @@ -4435,7 +4439,7 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack' as const, redirectUrl: 'https://example.com/callback', redirectUrlComplete: 'https://example.com/dashboard', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }; if (mode === 'popup') { await signIn.authenticateWithPopup({ ...params, popup: { location: { href: '' } } as Window }); @@ -4443,7 +4447,7 @@ describe('SignIn Slack workspace hint requests', () => { await signIn.authenticateWithRedirect(params); } expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); }); it.each(['redirect', 'popup'] as const)('refreshes the workspace on legacy %s continuation', async mode => { @@ -4454,10 +4458,10 @@ describe('SignIn Slack workspace hint requests', () => { redirectUrl: 'https://example.com/callback', redirectUrlComplete: 'https://example.com/dashboard', }; - const authenticate = (slackTeamId: string, continueSignIn = false) => + const authenticate = (slackTeamIdHint: string, continueSignIn = false) => mode === 'popup' - ? signIn.authenticateWithPopup({ ...params, slackTeamId, continueSignIn, popup }) - : signIn.authenticateWithRedirect({ ...params, slackTeamId, continueSignIn }); + ? signIn.authenticateWithPopup({ ...params, slackTeamIdHint, continueSignIn, popup }) + : signIn.authenticateWithRedirect({ ...params, slackTeamIdHint, continueSignIn }); fetchMock .mockResolvedValueOnce(jsonResponse({ response: workspaceResponse('T123ABC') })) @@ -4469,7 +4473,7 @@ describe('SignIn Slack workspace hint requests', () => { '/v1/client/sign_ins', '/v1/client/sign_ins/signin_slack/prepare_first_factor', ]); - expect([fields(0).get('slack_team_id'), fields(1).get('slack_team_id')]).toEqual(['T123ABC', 'T456DEF']); + expect([fields(0).get('slack_team_id_hint'), fields(1).get('slack_team_id_hint')]).toEqual(['T123ABC', 'T456DEF']); if (mode === 'popup') { expect(popup.location.href).toBe('https://slack.com/openid/connect/authorize?team=T456DEF'); } else { @@ -4480,9 +4484,9 @@ describe('SignIn Slack workspace hint requests', () => { }); it.each([ - { strategy: 'oauth_slack' as const, slackTeamId: '' }, - { strategy: 'oauth_google' as const, slackTeamId: 'T123ABC' }, - ])('forwards a rejected legacy continuation hint for $strategy with "$slackTeamId"', async params => { + { strategy: 'oauth_slack' as const, slackTeamIdHint: '' }, + { strategy: 'oauth_google' as const, slackTeamIdHint: 'T123ABC' }, + ])('forwards a rejected legacy continuation hint for $strategy with "$slackTeamIdHint"', async params => { fetchMock.mockResolvedValueOnce(jsonResponse({ errors: [{ code: 'form_param_value_invalid' }] }, 422)); const signIn = new SignIn(workspaceResponse('TOLD')); @@ -4498,7 +4502,7 @@ describe('SignIn Slack workspace hint requests', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); expect(fields().get('strategy')).toBe(params.strategy); - expect(fields().get('slack_team_id')).toBe(params.slackTeamId); + expect(fields().get('slack_team_id_hint')).toBe(params.slackTeamIdHint); expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); }); @@ -4528,7 +4532,7 @@ describe('SignIn Slack workspace hint requests', () => { continueSignIn: true, redirectUrl: 'https://example.com/callback', redirectUrlComplete: 'https://example.com/dashboard', - slackTeamId: 'T456DEF', + slackTeamIdHint: 'T456DEF', }; await expect(signIn.authenticateWithRedirect(params)).rejects.toMatchObject({ code: 'protect_check_required' }); @@ -4541,8 +4545,8 @@ describe('SignIn Slack workspace hint requests', () => { '/v1/client/sign_ins/signin_slack/protect_check', '/v1/client/sign_ins/signin_slack/prepare_first_factor', ]); - expect(fields(0).get('slack_team_id')).toBe('T456DEF'); - expect(fields(2).get('slack_team_id')).toBe('T456DEF'); + expect(fields(0).get('slack_team_id_hint')).toBe('T456DEF'); + expect(fields(2).get('slack_team_id_hint')).toBe('T456DEF'); expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledExactlyOnceWith( new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), ); @@ -4564,13 +4568,13 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T456DEF', + slackTeamIdHint: 'T456DEF', popup, }); expect(error).toBeNull(); expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); - expect(fields().get('slack_team_id')).toBe('T456DEF'); + expect(fields().get('slack_team_id_hint')).toBe('T456DEF'); if (popup) { expect(popup.location.href).toBe('https://slack.com/openid/connect/authorize?team=T456DEF'); } else { @@ -4581,9 +4585,9 @@ describe('SignIn Slack workspace hint requests', () => { }); it.each([ - { strategy: 'oauth_slack' as const, slackTeamId: '' }, - { strategy: 'oauth_google' as const, slackTeamId: 'T123ABC' }, - ])('forwards a rejected existing unified hint for $strategy with "$slackTeamId"', async params => { + { strategy: 'oauth_slack' as const, slackTeamIdHint: '' }, + { strategy: 'oauth_google' as const, slackTeamIdHint: 'T123ABC' }, + ])('forwards a rejected existing unified hint for $strategy with "$slackTeamIdHint"', async params => { fetchMock.mockResolvedValueOnce(jsonResponse({ errors: [{ code: 'form_param_value_invalid' }] }, 422)); const signIn = new SignIn({ ...new SignIn().__internal_toSnapshot(), @@ -4600,7 +4604,7 @@ describe('SignIn Slack workspace hint requests', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_slack/prepare_first_factor'); expect(fields().get('strategy')).toBe(params.strategy); - expect(fields().get('slack_team_id')).toBe(params.slackTeamId); + expect(fields().get('slack_team_id_hint')).toBe(params.slackTeamIdHint); expect(SignIn.clerk.__internal_windowNavigate).not.toHaveBeenCalled(); }); @@ -4641,7 +4645,7 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T456DEF', + slackTeamIdHint: 'T456DEF', oidcPrompt: 'consent', }); @@ -4651,7 +4655,7 @@ describe('SignIn Slack workspace hint requests', () => { '/v1/client/sign_ins/signin_slack/protect_check', '/v1/client/sign_ins/signin_slack/prepare_first_factor', ]); - expect(fields(0).get('slack_team_id')).toBe('T456DEF'); + expect(fields(0).get('slack_team_id_hint')).toBe('T456DEF'); expect(fields(2)).toEqual(fields(0)); expect(SignIn.clerk.__internal_windowNavigate).toHaveBeenCalledExactlyOnceWith( new URL('https://slack.com/openid/connect/authorize?team=T456DEF'), @@ -4670,7 +4674,7 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T456DEF', + slackTeamIdHint: 'T456DEF', }); expect(error).toMatchObject({ message: 'challenge failed' }); @@ -4686,12 +4690,12 @@ describe('SignIn Slack workspace hint requests', () => { continueSignIn: true, redirectUrl: 'https://example.com/callback', redirectUrlComplete: 'https://example.com/dashboard', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0][0].pathname).toBe('/v1/client/sign_ins/signin_existing/prepare_first_factor'); expect(fields().get('strategy')).toBe('enterprise_sso'); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); }); it('preserves unsupported legacy social first-factor preparation', () => { @@ -4709,18 +4713,18 @@ describe('SignIn Slack workspace hint requests', () => { it('creates fresh unified attempts for changed workspaces and then omits an absent hint', async () => { const signIn = new SignIn(); - for (const slackTeamId of ['T123ABC', 'T456DEF', undefined]) { + for (const slackTeamIdHint of ['T123ABC', 'T456DEF', undefined]) { const { error } = await signIn.__internal_future.sso({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId, + slackTeamIdHint, }); expect(error).toBeNull(); } expect(fetchMock).toHaveBeenCalledTimes(3); expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual(Array(3).fill('/v1/client/sign_ins')); - expect([0, 1, 2].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', null]); + expect([0, 1, 2].map(index => fields(index).get('slack_team_id_hint'))).toEqual(['T123ABC', 'T456DEF', null]); }); it('forwards a unified popup hint to the create request', async () => { @@ -4730,11 +4734,11 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', popup, }); expect(error).toBeNull(); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); expect(_futureAuthenticateWithPopup).toHaveBeenCalledWith(SignIn.clerk, expect.objectContaining({ popup })); }); @@ -4743,13 +4747,13 @@ describe('SignIn Slack workspace hint requests', () => { strategy, redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(error).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(strategy === 'enterprise_sso' ? 2 : 1); for (let index = 0; index < fetchMock.mock.calls.length; index++) { expect(fields(index).get('strategy')).toBe(strategy); - expect(fields(index).get('slack_team_id')).toBe('T123ABC'); + expect(fields(index).get('slack_team_id_hint')).toBe('T123ABC'); } }); @@ -4777,12 +4781,12 @@ describe('SignIn Slack workspace hint requests', () => { strategy: 'enterprise_sso', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(error).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fields(0).get('slack_team_id')).toBe('T123ABC'); - expect(fields(1).get('slack_team_id')).toBe('T123ABC'); + expect(fields(0).get('slack_team_id_hint')).toBe('T123ABC'); + expect(fields(1).get('slack_team_id_hint')).toBe('T123ABC'); expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual( Array(2).fill('/v1/client/sign_ins/signin_existing/prepare_first_factor'), ); diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index 5b56b7b5454..67668057776 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -2545,12 +2545,17 @@ describe('SignUp Slack workspace hint requests', () => { if (method !== 'create') { signUp.id = 'signup_slack'; } - for (const slackTeamId of ['T123ABC', 'T456DEF', '', undefined]) { - await signUp[method]({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', slackTeamId }); + for (const slackTeamIdHint of ['T123ABC', 'T456DEF', '', undefined]) { + await signUp[method]({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/callback', slackTeamIdHint }); } expect(fetchMock).toHaveBeenCalledTimes(4); - expect([0, 1, 2, 3].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', '', null]); - expect(fetchMock.mock.calls.every((_, index) => !fields(index).has('slackTeamId'))).toBe(true); + expect([0, 1, 2, 3].map(index => fields(index).get('slack_team_id_hint'))).toEqual([ + 'T123ABC', + 'T456DEF', + '', + null, + ]); + expect(fetchMock.mock.calls.every((_, index) => !fields(index).has('slackTeamIdHint'))).toBe(true); const expectedPath = method === 'create' ? '/v1/client/sign_ups' @@ -2575,7 +2580,7 @@ describe('SignUp Slack workspace hint requests', () => { strategy: 'oauth_slack' as const, redirectUrl: 'https://example.com/callback', redirectUrlComplete: 'https://example.com/dashboard', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', continueSignUp, }; if (popup) { @@ -2584,7 +2589,7 @@ describe('SignUp Slack workspace hint requests', () => { await signUp.authenticateWithRedirect(params); } expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); expect(fetchMock.mock.calls[0][0].pathname).toBe( continueSignUp ? '/v1/client/sign_ups/signup_existing' : '/v1/client/sign_ups', ); @@ -2593,17 +2598,17 @@ describe('SignUp Slack workspace hint requests', () => { it('forwards unified create and update hints without retaining the previous workspace', async () => { const signUp = new SignUp(); - for (const slackTeamId of ['T123ABC', 'T456DEF', undefined]) { + for (const slackTeamIdHint of ['T123ABC', 'T456DEF', undefined]) { const { error } = await signUp.__internal_future.sso({ strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId, + slackTeamIdHint, }); expect(error).toBeNull(); } expect(fetchMock).toHaveBeenCalledTimes(3); - expect([0, 1, 2].map(index => fields(index).get('slack_team_id'))).toEqual(['T123ABC', 'T456DEF', null]); + expect([0, 1, 2].map(index => fields(index).get('slack_team_id_hint'))).toEqual(['T123ABC', 'T456DEF', null]); expect(fetchMock.mock.calls.map(([url]) => url.pathname)).toEqual([ '/v1/client/sign_ups', '/v1/client/sign_ups/signup_slack', @@ -2619,11 +2624,11 @@ describe('SignUp Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', popup, }); expect(error).toBeNull(); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); expect(_futureAuthenticateWithPopup).toHaveBeenCalledWith(SignUp.clerk, expect.objectContaining({ popup })); }); @@ -2632,12 +2637,12 @@ describe('SignUp Slack workspace hint requests', () => { strategy, redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(error).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fields().get('strategy')).toBe(strategy); - expect(fields().get('slack_team_id')).toBe('T123ABC'); + expect(fields().get('slack_team_id_hint')).toBe('T123ABC'); }); it('preserves a unified hint when an environment change requires a retry', async () => { @@ -2651,12 +2656,12 @@ describe('SignUp Slack workspace hint requests', () => { strategy: 'oauth_slack', redirectUrl: 'https://example.com/dashboard', redirectCallbackUrl: 'https://example.com/callback', - slackTeamId: 'T123ABC', + slackTeamIdHint: 'T123ABC', }); expect(error).toBeNull(); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(fields(0).get('slack_team_id')).toBe('T123ABC'); - expect(fields(1).get('slack_team_id')).toBe('T123ABC'); + expect(fields(0).get('slack_team_id_hint')).toBe('T123ABC'); + expect(fields(1).get('slack_team_id_hint')).toBe('T123ABC'); expect(SignUp.clerk.__internal_environment?.reload).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index 7e7b3259fed..4b52a8e95a9 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -79,7 +79,7 @@ export type AuthenticateWithRedirectParams = { /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; /** * Internal OAuth callback context, consumed only when an OAuth transport completes the diff --git a/packages/shared/src/types/signInCommon.ts b/packages/shared/src/types/signInCommon.ts index a90cf4f3887..8f0d39e194c 100644 --- a/packages/shared/src/types/signInCommon.ts +++ b/packages/shared/src/types/signInCommon.ts @@ -148,7 +148,7 @@ export type SignInCreateParams = ( /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; } | { strategy: TicketStrategy; diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 09f05bc83b5..079e414e242 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -38,7 +38,7 @@ export interface SignInFutureCreateParams { /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; /** * The URL that the user will be redirected to, after successful authorization from the OAuth provider and Clerk sign-in. */ @@ -250,7 +250,7 @@ export interface SignInFutureSSOParams { /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental diff --git a/packages/shared/src/types/signUpCommon.ts b/packages/shared/src/types/signUpCommon.ts index 74cc0741277..ebdd07a98a1 100644 --- a/packages/shared/src/types/signUpCommon.ts +++ b/packages/shared/src/types/signUpCommon.ts @@ -82,7 +82,7 @@ export type PrepareVerificationParams = /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; } | { strategy: EnterpriseSSOStrategy; @@ -141,7 +141,7 @@ export type SignUpCreateParams = Partial< /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; channel: PhoneCodeChannel; locale?: string; /** diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index 69de5c02711..abc9d1bf4dc 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -251,7 +251,7 @@ export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { /** * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. */ - slackTeamId?: string; + slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental From c1f780efdc4caee51766b477dd306b74396d7503 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 27 Oct 2026 13:07:23 -0400 Subject: [PATCH 5/7] fix(types): scope Slack workspace hints to oauth_slack --- .../clerk-js/src/core/resources/SignIn.ts | 10 +++-- .../clerk-js/src/core/resources/SignUp.ts | 7 ++-- packages/shared/src/types/redirects.ts | 15 ++++--- packages/shared/src/types/signInCommon.ts | 15 ++++--- packages/shared/src/types/signInFuture.ts | 34 +++++++++------ packages/shared/src/types/signUpCommon.ts | 41 ++++++++++++------- packages/shared/src/types/signUpFuture.ts | 17 +++++--- 7 files changed, 89 insertions(+), 50 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 83caee52941..f5d617c1f26 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -460,12 +460,13 @@ export class SignIn extends BaseResource implements SignInResource { const shouldCreateSignIn = !this.id || !continueSignIn; if (shouldCreateSignIn) { await this.create({ - strategy, identifier, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, - slackTeamIdHint, + ...(params.strategy === 'oauth_slack' + ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } + : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), }); if (isChallengePending()) { @@ -1429,11 +1430,12 @@ class SignInFuture implements SignInFutureResource { if (shouldCreateSignIn) { await this._create( { - strategy, ...routes, identifier, oidcPrompt, - slackTeamIdHint, + ...(params.strategy === 'oauth_slack' + ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } + : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), }, { resolveProtectCheck: false }, ); diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index b335d7cfe14..017e27c1b04 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -446,13 +446,11 @@ export class SignUp extends BaseResource implements SignUpResource { const { redirectUrl, redirectUrlComplete, - strategy, continueSignUp = false, unsafeMetadata, emailAddress, legalAccepted, oidcPrompt, - slackTeamIdHint, enterpriseConnectionId, } = params; @@ -460,14 +458,15 @@ export class SignUp extends BaseResource implements SignUpResource { const authenticateFn = () => { const authParams = { - strategy, redirectUrl: redirectUrlWithAuthToken, actionCompleteRedirectUrl: redirectUrlComplete, unsafeMetadata, emailAddress, legalAccepted, oidcPrompt, - slackTeamIdHint, + ...(params.strategy === 'oauth_slack' + ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } + : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), enterpriseConnectionId, }; return continueSignUp && this.id ? this.update(authParams) : this.create(authParams); diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index 4b52a8e95a9..a88b3f458c2 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -76,10 +76,6 @@ export type AuthenticateWithRedirectParams = { * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; /** * Internal OAuth callback context, consumed only when an OAuth transport completes the @@ -94,7 +90,16 @@ export type AuthenticateWithRedirectParams = { * @experimental */ enterpriseConnectionId?: string; -}; +} & ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } +); export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null }; diff --git a/packages/shared/src/types/signInCommon.ts b/packages/shared/src/types/signInCommon.ts index 8f0d39e194c..fbf95ace3ef 100644 --- a/packages/shared/src/types/signInCommon.ts +++ b/packages/shared/src/types/signInCommon.ts @@ -145,10 +145,6 @@ export type SignInCreateParams = ( identifier?: string; oidcPrompt?: string; oidcLoginHint?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; } | { strategy: TicketStrategy; @@ -202,7 +198,16 @@ export type SignInCreateParams = ( * An IANA timezone for this sign-in. Defaults to the browser's timezone when omitted, if available. */ timezone?: string; -}; +} & ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } + ); export type ResetPasswordParams = { password: string; diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 079e414e242..e2834193a58 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -8,7 +8,7 @@ import type { VerificationResource } from './verification'; import type { Web3Provider } from './web3'; /** @generateWithEmptyComment */ -export interface SignInFutureCreateParams { +export type SignInFutureCreateParams = { /** The timezone to assign to the user. If omitted, defaults to the browser's timezone. */ timezone?: string; /** @@ -35,10 +35,6 @@ export interface SignInFutureCreateParams { * The value to pass to the [OIDC `login_hint` parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=login_hint,in%20\(if%20necessary\).) in the generated OAuth redirect URL. */ oidcLoginHint?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; /** * The URL that the user will be redirected to, after successful authorization from the OAuth provider and Clerk sign-in. */ @@ -55,7 +51,16 @@ export interface SignInFutureCreateParams { * When set to `true`, if a user does not exist, the sign-up will prepare a transfer to sign up a new account. If bot sign-up protection is enabled, captcha will also be required on sign in. */ signUpIfMissing?: boolean; -} +} & ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } +); /** Parameters for submitting a password to sign-in. */ export type SignInFuturePasswordParams = { @@ -216,7 +221,7 @@ export interface SignInFutureResetPasswordPhoneCodeVerifyParams { } /** @generateWithEmptyComment */ -export interface SignInFutureSSOParams { +export type SignInFutureSSOParams = { /** * The strategy to use for authentication. */ @@ -247,10 +252,6 @@ export interface SignInFutureSSOParams { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental @@ -260,7 +261,16 @@ export interface SignInFutureSSOParams { * The unique identifier of the user. Only supported with the `enterprise_sso` strategy. */ identifier?: string; -} +} & ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } +); /** @generateWithEmptyComment */ export interface SignInFutureMFAPhoneCodeVerifyParams { diff --git a/packages/shared/src/types/signUpCommon.ts b/packages/shared/src/types/signUpCommon.ts index ebdd07a98a1..c39a672f55b 100644 --- a/packages/shared/src/types/signUpCommon.ts +++ b/packages/shared/src/types/signUpCommon.ts @@ -19,7 +19,7 @@ import type { TicketStrategy, Web3Strategy, } from './strategies'; -import type { SnakeToCamel } from './utils'; +import type { SnakeToCamel, Without } from './utils'; import type { VerificationResource } from './verification'; /** @inline */ @@ -58,7 +58,7 @@ export interface ProtectCheckResource { uiHints?: Record; } -export type PrepareVerificationParams = +export type PrepareVerificationParams = ( | { strategy: EmailCodeStrategy; } @@ -79,16 +79,23 @@ export type PrepareVerificationParams = actionCompleteRedirectUrl?: string; oidcPrompt?: string; oidcLoginHint?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; } | { strategy: EnterpriseSSOStrategy; redirectUrl?: string; actionCompleteRedirectUrl?: string; - }; + } +) & + ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } + ); export type AttemptVerificationParams = | { @@ -138,10 +145,6 @@ export type SignUpCreateParams = Partial< legalAccepted: boolean; oidcPrompt: string; oidcLoginHint: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; channel: PhoneCodeChannel; locale?: string; /** @@ -149,9 +152,19 @@ export type SignUpCreateParams = Partial< */ timezone?: string; } & Omit>, 'legalAccepted'> ->; - -export type SignUpUpdateParams = Omit; +> & + ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } + ); + +export type SignUpUpdateParams = Without; /** * @deprecated Use `SignUpAuthenticateWithWeb3Params` instead. diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index abc9d1bf4dc..9001e170e3b 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -217,7 +217,7 @@ export interface SignUpFuturePhoneCodeVerifyParams { } /** @generateWithEmptyComment */ -export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { +export type SignUpFutureSSOParams = SignUpFutureAdditionalParams & { /** * The strategy to use for authentication. Either [`OAuthStrategy`](https://clerk.com/docs/reference/types/sso#oauthstrategy) or [`EnterpriseSSOStrategy`](https://clerk.com/docs/reference/types/sso#enterprisessostrategy). */ @@ -248,10 +248,6 @@ export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental @@ -261,7 +257,16 @@ export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { * The email address to use for targeting an enterprise connection at sign-up. */ emailAddress?: string; -} +} & ( + | { + strategy: 'oauth_slack'; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; + } + | { slackTeamIdHint?: never } + ); /** @generateWithEmptyComment */ export interface SignUpFutureTicketParams extends SignUpFutureAdditionalParams { From 3fdb0dba561a0ed0205ece5b34a282b961bb6e1a Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 27 Oct 2026 13:28:16 -0400 Subject: [PATCH 6/7] fix(types): align Slack hints with existing OAuth options --- .../clerk-js/src/core/resources/SignIn.ts | 10 ++--- .../clerk-js/src/core/resources/SignUp.ts | 7 ++-- packages/shared/src/types/redirects.ts | 15 +++---- packages/shared/src/types/signInCommon.ts | 15 +++---- packages/shared/src/types/signInFuture.ts | 34 ++++++--------- packages/shared/src/types/signUpCommon.ts | 41 +++++++------------ packages/shared/src/types/signUpFuture.ts | 17 +++----- 7 files changed, 50 insertions(+), 89 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index f5d617c1f26..83caee52941 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -460,13 +460,12 @@ export class SignIn extends BaseResource implements SignInResource { const shouldCreateSignIn = !this.id || !continueSignIn; if (shouldCreateSignIn) { await this.create({ + strategy, identifier, redirectUrl, actionCompleteRedirectUrl, oidcPrompt, - ...(params.strategy === 'oauth_slack' - ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } - : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), + slackTeamIdHint, }); if (isChallengePending()) { @@ -1430,12 +1429,11 @@ class SignInFuture implements SignInFutureResource { if (shouldCreateSignIn) { await this._create( { + strategy, ...routes, identifier, oidcPrompt, - ...(params.strategy === 'oauth_slack' - ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } - : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), + slackTeamIdHint, }, { resolveProtectCheck: false }, ); diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 017e27c1b04..b335d7cfe14 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -446,11 +446,13 @@ export class SignUp extends BaseResource implements SignUpResource { const { redirectUrl, redirectUrlComplete, + strategy, continueSignUp = false, unsafeMetadata, emailAddress, legalAccepted, oidcPrompt, + slackTeamIdHint, enterpriseConnectionId, } = params; @@ -458,15 +460,14 @@ export class SignUp extends BaseResource implements SignUpResource { const authenticateFn = () => { const authParams = { + strategy, redirectUrl: redirectUrlWithAuthToken, actionCompleteRedirectUrl: redirectUrlComplete, unsafeMetadata, emailAddress, legalAccepted, oidcPrompt, - ...(params.strategy === 'oauth_slack' - ? { strategy: 'oauth_slack' as const, slackTeamIdHint: params.slackTeamIdHint } - : { strategy: params.strategy, slackTeamIdHint: params.slackTeamIdHint }), + slackTeamIdHint, enterpriseConnectionId, }; return continueSignUp && this.id ? this.update(authParams) : this.create(authParams); diff --git a/packages/shared/src/types/redirects.ts b/packages/shared/src/types/redirects.ts index a88b3f458c2..4b52a8e95a9 100644 --- a/packages/shared/src/types/redirects.ts +++ b/packages/shared/src/types/redirects.ts @@ -76,6 +76,10 @@ export type AuthenticateWithRedirectParams = { * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; /** * Internal OAuth callback context, consumed only when an OAuth transport completes the @@ -90,16 +94,7 @@ export type AuthenticateWithRedirectParams = { * @experimental */ enterpriseConnectionId?: string; -} & ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } -); +}; export type AuthenticateWithPopupParams = AuthenticateWithRedirectParams & { popup: Window | null }; diff --git a/packages/shared/src/types/signInCommon.ts b/packages/shared/src/types/signInCommon.ts index fbf95ace3ef..8f0d39e194c 100644 --- a/packages/shared/src/types/signInCommon.ts +++ b/packages/shared/src/types/signInCommon.ts @@ -145,6 +145,10 @@ export type SignInCreateParams = ( identifier?: string; oidcPrompt?: string; oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; } | { strategy: TicketStrategy; @@ -198,16 +202,7 @@ export type SignInCreateParams = ( * An IANA timezone for this sign-in. Defaults to the browser's timezone when omitted, if available. */ timezone?: string; -} & ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } - ); +}; export type ResetPasswordParams = { password: string; diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index e2834193a58..079e414e242 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -8,7 +8,7 @@ import type { VerificationResource } from './verification'; import type { Web3Provider } from './web3'; /** @generateWithEmptyComment */ -export type SignInFutureCreateParams = { +export interface SignInFutureCreateParams { /** The timezone to assign to the user. If omitted, defaults to the browser's timezone. */ timezone?: string; /** @@ -35,6 +35,10 @@ export type SignInFutureCreateParams = { * The value to pass to the [OIDC `login_hint` parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=login_hint,in%20\(if%20necessary\).) in the generated OAuth redirect URL. */ oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; /** * The URL that the user will be redirected to, after successful authorization from the OAuth provider and Clerk sign-in. */ @@ -51,16 +55,7 @@ export type SignInFutureCreateParams = { * When set to `true`, if a user does not exist, the sign-up will prepare a transfer to sign up a new account. If bot sign-up protection is enabled, captcha will also be required on sign in. */ signUpIfMissing?: boolean; -} & ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } -); +} /** Parameters for submitting a password to sign-in. */ export type SignInFuturePasswordParams = { @@ -221,7 +216,7 @@ export interface SignInFutureResetPasswordPhoneCodeVerifyParams { } /** @generateWithEmptyComment */ -export type SignInFutureSSOParams = { +export interface SignInFutureSSOParams { /** * The strategy to use for authentication. */ @@ -252,6 +247,10 @@ export type SignInFutureSSOParams = { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental @@ -261,16 +260,7 @@ export type SignInFutureSSOParams = { * The unique identifier of the user. Only supported with the `enterprise_sso` strategy. */ identifier?: string; -} & ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } -); +} /** @generateWithEmptyComment */ export interface SignInFutureMFAPhoneCodeVerifyParams { diff --git a/packages/shared/src/types/signUpCommon.ts b/packages/shared/src/types/signUpCommon.ts index c39a672f55b..ebdd07a98a1 100644 --- a/packages/shared/src/types/signUpCommon.ts +++ b/packages/shared/src/types/signUpCommon.ts @@ -19,7 +19,7 @@ import type { TicketStrategy, Web3Strategy, } from './strategies'; -import type { SnakeToCamel, Without } from './utils'; +import type { SnakeToCamel } from './utils'; import type { VerificationResource } from './verification'; /** @inline */ @@ -58,7 +58,7 @@ export interface ProtectCheckResource { uiHints?: Record; } -export type PrepareVerificationParams = ( +export type PrepareVerificationParams = | { strategy: EmailCodeStrategy; } @@ -79,23 +79,16 @@ export type PrepareVerificationParams = ( actionCompleteRedirectUrl?: string; oidcPrompt?: string; oidcLoginHint?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; } | { strategy: EnterpriseSSOStrategy; redirectUrl?: string; actionCompleteRedirectUrl?: string; - } -) & - ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } - ); + }; export type AttemptVerificationParams = | { @@ -145,6 +138,10 @@ export type SignUpCreateParams = Partial< legalAccepted: boolean; oidcPrompt: string; oidcLoginHint: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; channel: PhoneCodeChannel; locale?: string; /** @@ -152,19 +149,9 @@ export type SignUpCreateParams = Partial< */ timezone?: string; } & Omit>, 'legalAccepted'> -> & - ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } - ); - -export type SignUpUpdateParams = Without; +>; + +export type SignUpUpdateParams = Omit; /** * @deprecated Use `SignUpAuthenticateWithWeb3Params` instead. diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index 9001e170e3b..abc9d1bf4dc 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -217,7 +217,7 @@ export interface SignUpFuturePhoneCodeVerifyParams { } /** @generateWithEmptyComment */ -export type SignUpFutureSSOParams = SignUpFutureAdditionalParams & { +export interface SignUpFutureSSOParams extends SignUpFutureAdditionalParams { /** * The strategy to use for authentication. Either [`OAuthStrategy`](https://clerk.com/docs/reference/types/sso#oauthstrategy) or [`EnterpriseSSOStrategy`](https://clerk.com/docs/reference/types/sso#enterprisessostrategy). */ @@ -248,6 +248,10 @@ export type SignUpFutureSSOParams = SignUpFutureAdditionalParams & { * The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ oidcPrompt?: string; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; /** * The identifier of the enterprise connection to target when using the `enterprise_sso` strategy. * @experimental @@ -257,16 +261,7 @@ export type SignUpFutureSSOParams = SignUpFutureAdditionalParams & { * The email address to use for targeting an enterprise connection at sign-up. */ emailAddress?: string; -} & ( - | { - strategy: 'oauth_slack'; - /** - * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. - */ - slackTeamIdHint?: string; - } - | { slackTeamIdHint?: never } - ); +} /** @generateWithEmptyComment */ export interface SignUpFutureTicketParams extends SignUpFutureAdditionalParams { From 53c98c0c7b1c9658216330688660beaa945a6c8d Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 27 Oct 2026 13:41:55 -0400 Subject: [PATCH 7/7] fix(types): expose Slack hint on unified sign-up creation --- packages/shared/src/types/signUpFuture.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index abc9d1bf4dc..479044078c4 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -65,6 +65,10 @@ export interface SignUpFutureCreateParams extends SignUpFutureAdditionalParams { | GoogleOneTapStrategy | AppleIdTokenStrategy | PhoneCodeStrategy; + /** + * Optional per-attempt Slack workspace hint. Only supported with the `oauth_slack` strategy. + */ + slackTeamIdHint?: string; /** * The user's email address. Only supported if [Email address](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options#email) is enabled in the instance settings. Keep in mind that the email address requires an extra verification process. */