Bridge Training
Auth

Hooks and types

Store-backed hooks and the vendor-free types every app codes against.

Import from @workspace/auth. Every hook reads the auth store, so it works the same under Clerk and under the simulated provider, and it never imports the vendor. The types alone are also exposed by @workspace/auth/types.

useAuthPort

Returns a stable AuthPort. The object only changes when the user id, the loading flag, the signed-in flag or the impersonation changes.

import { useAuthPort } from '@workspace/auth';

function DevOnly({ children }: { children: ReactNode }) {
  const auth = useAuthPort();
  return isDevUser(auth.getUserId()) ? children : null;
}

The app hands the same object to TanStack Router's context so route beforeLoad guards can call auth.isAuthenticated() and redirect to the sign-in page.

AuthPort

Prop

Type

getUserId() is the effective identity: the impersonated subject while an impersonation runs, else the signed-in user. getActorId() is who really holds the session. Read the former for data, the latter for anything that must name the human in front of the screen. See Impersonation.

IAuthService and useAuthService are deprecated aliases of AuthPort and useAuthPort.

useAuthSession

Returns the AuthSession or null. It only updates when the session id changes, not on every token refresh.

import { useAuthSession } from '@workspace/auth';

const session = useAuthSession();
const getToken = useMemo(
  () => () => session?.getToken() ?? Promise.resolve(null),
  [session],
);

AuthSession

Prop

Type

getToken() returns the bearer the API verifies with verifyToken. Under the simulated provider it resolves to a simulated-token:<userId> string.

useAuthProfile

Returns what a surface may show of the signed-in user, or null when nobody is signed in: the avatar menu of a public page reads it, signupSource included to send the user to the space they signed up on.

import { useAuthProfile } from '@workspace/auth';

const profile = useAuthProfile();

Prop

Type

useImpersonation

Returns an ImpersonationState, never null. When nothing is active every field but isImpersonating is null.

import { useImpersonation } from '@workspace/auth';

const { isImpersonating, subjectId, actorLabel, startedAt } = useImpersonation();

Prop

Type

useImpersonationActions

Returns { start(token, subjectId), stop() } bound to the store. start throws when nobody is signed in. These are store-level only: the HTTP calls that mint the token or log the stop live in the app.

import { useImpersonationActions } from '@workspace/auth';

const actions = useImpersonationActions();
actions.start(handoff.token, handoff.subjectId);
actions.stop();

useAvatarUpdate

Uploads or removes the signed-in user's avatar through whichever provider populated the store.

import { useAvatarUpdate } from '@workspace/auth';

const { uploadAvatar, removeAvatar, isPending, error } = useAvatarUpdate();

await uploadAvatar(file);   // validates, then { imageUrl }
await removeAvatar();       // { imageUrl } of the provider default

Both calls reject with an AvatarUpdateError, also exposed through error.

Avatar validation

validateAvatarFile(file) throws before any upload when the MIME type is not in ALLOWED_AVATAR_MIME_TYPES (image/png, image/jpeg, image/webp) or the size exceeds MAX_AVATAR_SIZE_BYTES (5 MiB). uploadAvatar calls it for you; call it yourself to reject a file on selection.

AvatarUpdateError extends AppError from @workspace/training with isExpected = true and one of these codes:

CodeRaised when
AVATAR_INVALID_TYPEMIME type not allowed
AVATAR_FILE_TOO_LARGEFile above the size cap
AVATAR_UPLOAD_FAILEDThe provider refused the upload
AVATAR_PERSIST_FAILEDThe removal failed
UNAUTHORIZEDNo signed-in user

AuthComponents

The surface a host mounts. Both createClerkAuth and createSimulatedAuth return it.

Prop

Type

Provider suspends its children until the store reports isLoaded, rendering pending meanwhile. Mount it once, above everything that calls a hook from this package.

Testing a consumer

Components that only need an AuthPort can take a hand-written one, no provider required:

import type { AuthPort } from '@workspace/auth';

const anonymous: AuthPort = {
  getUserId: () => null,
  getActorId: () => null,
  getImpersonationToken: () => null,
  isLoaded: () => true,
  isAuthenticated: () => false,
  signOut: async () => {},
};

Components that call the hooks need the store loaded: render them inside a simulated provider.