Skip to content

feat(react-auth): add createMultiAuth for multi-provider support#241

Merged
panz3r merged 5 commits intomainfrom
copilot/add-multi-provider-support
Mar 28, 2026
Merged

feat(react-auth): add createMultiAuth for multi-provider support#241
panz3r merged 5 commits intomainfrom
copilot/add-multi-provider-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

Affected Package(s)

  • @forward-software/react-auth (lib)
  • @forward-software/react-auth-google (packages/google-signin)
  • Examples
  • CI/CD / Repository configuration

Related Issue(s)

Motivation

The library previously supported only a single AuthClient per createAuth call. This adds createMultiAuth, enabling multiple auth providers to coexist under a single AuthProvider, each addressable by a typed string key. createAuth has also been refactored to be a thin wrapper over createMultiAuth to share the same initialisation logic. As a result of the refactor, the use-sync-external-store runtime dependency has been eliminated — the library now has zero runtime dependencies.

Description of Changes

lib/src/auth.tsx

  • Added createMultiAuth<M, E>(authClientsMap: M) — returns { AuthProvider, authClients, useAuth }. AuthProvider initialises all clients in parallel, honours LoadingComponent/ErrorComponent (loading until all settle, error if any fail). useAuth(id) is generic over keyof M — return type is EnhancedAuthClient<M[K], E> (per-key precision). Runtime guard in useAuth throws a descriptive error for unknown ids.
  • Fixed wrapAuthClient global prototype mutation: replaced Object.setPrototypeOf(AuthClientEnhancements.prototype, authClient) with a per-instance prototype built via Object.create(authClient) + copying enhancement method descriptors. Direct calls to raw AuthClient interface methods (e.g. authClients.primary.onLogin()) now correctly dispatch to each client's own implementation.
  • Each client's init() call is wrapped with .catch((): boolean => false) so that a rejection from one client (e.g. an onPostInit error) does not short-circuit the others — all clients always get the chance to finish initialising before the provider state is updated.
  • Reduce accumulator for enhancedClientsMap now uses Object.create(null) to prevent prototype pollution from user-controlled keys (e.g. __proto__).
  • Atomic init state ({ allInitialized, failed } single useState) avoids a flash-of-children on init failure in React <18.
  • Refactored createAuth to be a thin wrapper over createMultiAuth({ default: authClient }). The public API (AuthProvider, authClient, useAuthClient) is unchanged. Removed the now-unnecessary useSyncExternalStore subscription from the provider and the internal AuthContext/AuthProviderState types.

lib/package.json

  • Removed use-sync-external-store runtime dependency and @types/use-sync-external-store devDependency — the library no longer calls useSyncExternalStore internally. The subscribe/getSnapshot methods on EnhancedAuthClient remain as public API for consumers to use directly with useSyncExternalStore if needed.

lib/src/index.ts

  • Exports createMultiAuth and EnhancedAuthClient (type was missing from the public barrel).

lib/README.md

  • Added a full createMultiAuth section with setup code, AuthProvider props explanation, and a useAuth hook example with multiple providers.

pnpm-lock.yaml

  • Merged origin/main into the branch and resolved lockfile conflicts via pnpm install --no-frozen-lockfile + pnpm dedupe to incorporate the removal of use-sync-external-store alongside upstream changes.

lib/test/multiAuth.spec.tsx — 13 new tests covering:

  • Return shape, all clients in authClients, useAuth outside provider, per-id client identity, all onInit called, init error handling, LoadingComponent, ErrorComponent, context access by both ids, per-client onLogin routing isolation (regression guard for the prototype fix), empty map, unknown-id runtime error.

lib/test/provider.spec.tsx

  • Updated LoadingComponent test: the old implementation showed LoadingComponent forever when init failed without an ErrorComponent (artifact of useSyncExternalStore). The new correct behaviour hides LoadingComponent once init settles; the test now asserts visibility before init completes, consistent with the multiAuth spec pattern.

Breaking Changes

None

How to Test

  1. CI Checks: Verify that all automated tests (Vitest) and build steps pass successfully on this PR.
  2. Local Verification (Optional):
    • Run pnpm install to install dependencies.
    • Run pnpm --filter @forward-software/react-auth test to run tests for the affected package.
    • Run pnpm --filter @forward-software/react-auth build to verify the build succeeds.
    • Run pnpm --filter @forward-software/react-auth lint to check for linting errors.

Checklist

  • My code follows the project's style guidelines
  • I have added or updated tests to cover the changes
  • I have updated relevant documentation
  • All tests are passing locally
  • CI checks are passing
  • I have reviewed my own code and lock file changes
  • I have checked for any potential security implications
  • I have verified the changes work as expected
  • My commit messages follow Conventional Commits format

Notes for Reviewers

  • The wrapAuthClient prototype fix changes instance identity: enhancedClient instanceof AuthClientEnhancements now returns false (prototype chain no longer includes AuthClientEnhancements.prototype). Nothing in the library or tests relies on instanceof, but worth noting if consumers do.
  • createAuth now delegates to createMultiAuth internally. The useSyncExternalStore subscription that previously drove the provider's reactive re-renders on auth state changes has been removed. Auth state updates (login, logout, refresh) are still fully observable via useSyncExternalStore(client.subscribe, client.getSnapshot) directly on the returned authClient — the provider simply no longer re-renders all children automatically on every state change. This is a deliberate improvement: components subscribe to exactly what they need rather than getting blanket re-renders from the provider.
  • The library now has zero runtime dependencies. Consumers on React < 18 who want to use subscribe/getSnapshot with useSyncExternalStore will need to install use-sync-external-store themselves (or use React 18+ where it is built in).
Original prompt

Can you orchestrate the full development cycle of a new feature for this library?

Currently the library only supports a single AuthClient at a time with the common AuthProvider component generated through createAuth, we'd like to add a new multi-provider support so that a user can create an [AuthProvider, useAuth] tuple with multiple providers and recall each by identifier.
The new createMultiAuth method should accept a map of <'id', AuthClient instance> and when using the returned useAuth hook, it can specify which AuthClient instance to use based on the 'id' provided as input to the hook itself.

If possible we can also reuse this same pattern for the current createAuth method by wrapping the new `createMultiAuth' one and providing wrapper function with a 'default' id.

Custom agent used: Beppe - Orchestrator
Use when: coordinating a full development cycle across planning, implementation, review, and refinement in this monorepo by delegating to the Developer and Reviewer agents.


⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

Copilot AI changed the title [WIP] Add multi-provider support to AuthClient feat(react-auth): add createMultiAuth for multi-provider support Mar 28, 2026
Copilot AI requested a review from panz3r March 28, 2026 12:59
@panz3r panz3r requested review from Copilot and removed request for panz3r March 28, 2026 13:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new createMultiAuth API to support multiple AuthClient instances under a single provider, while also fixing a multi-client correctness issue in wrapAuthClient caused by global prototype mutation.

Changes:

  • Added createMultiAuth with keyed useAuth(id) and a shared AuthProvider that initializes all clients.
  • Fixed wrapAuthClient to avoid cross-client method bleed by building a per-instance prototype chain.
  • Exported createMultiAuth / EnhancedAuthClient from the public barrel and added comprehensive Vitest coverage.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/src/auth.tsx Adds createMultiAuth and updates wrapAuthClient to use per-instance prototype wiring.
lib/src/index.ts Re-exports createMultiAuth and EnhancedAuthClient from the package entrypoint.
lib/test/multiAuth.spec.tsx Adds tests covering multi-client init, context access by id, error/loading components, and client isolation regression.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@panz3r
Copy link
Copy Markdown
Member

panz3r commented Mar 28, 2026

@copilot have a look at review comments and fix reported issues if considered correct.

Also update documentation in README.md to feature sample for the multi-provider functionality.

Also update the existing createAuth method to leverage the new multi-auth method (or describe in more details why that would be a poor choice)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot have a look at review comments and fix reported issues if considered correct.

Also update documentation in README.md to feature sample for the multi-provider functionality....

Done in ae29a68. Here's what was addressed:

Review comments fixed:

  • Object.create(null) is now used as the reduce accumulator to prevent prototype pollution from reserved keys like __proto__
  • Promise.all replaced with per-client .catch((): boolean => false) wrapping — every client always gets to finish initialising with no short-circuit on rejection (same semantics as Promise.allSettled, ES6-compatible without tsconfig changes)

createAuth refactored to wrap createMultiAuth: createAuth is now a thin wrapper over createMultiAuth({ default: authClient }). The public API (AuthProvider, authClient, useAuthClient) is unchanged. The old useSyncExternalStore subscription in the provider and the internal AuthContext/AuthProviderState types were removed — init state is now managed uniformly by createMultiAuth's useState-based approach, which is both simpler and fixes the edge case where LoadingComponent would previously show forever when init failed without an ErrorComponent.

lib/README.md updated with a full createMultiAuth section covering setup, AuthProvider props, and the useAuth hook with typed code examples.

Copilot AI requested a review from panz3r March 28, 2026 13:23
Copy link
Copy Markdown
Member

@panz3r panz3r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot looks great 👍

Now that useSyncExternalStore has been removed, can we perhaps remove the external dependency altogether or is that still used somewhere else?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot looks great 👍

Now that useSyncExternalStore has been removed, can we perhaps remove the external dependency altogether or is that still used somewhere else?

Yes, it's been removed in b3ed4c2. Both use-sync-external-store (runtime dep) and @types/use-sync-external-store (devDep) have been dropped from lib/package.json and the lockfile has been updated. The subscribe/getSnapshot methods on EnhancedAuthClient remain as public API — consumers who want to use them with useSyncExternalStore on React < 18 would install use-sync-external-store themselves, but the library itself has zero runtime dependencies now.

Copilot AI requested a review from panz3r March 28, 2026 13:37
@panz3r panz3r requested a review from IronTony March 28, 2026 13:59
@panz3r panz3r marked this pull request as ready for review March 28, 2026 13:59
@panz3r panz3r added the enhancement New feature or request label Mar 28, 2026
@panz3r
Copy link
Copy Markdown
Member

panz3r commented Mar 28, 2026

@copilot please resolve conflicts in lock-file.

Running pnpm i and pnpm dedupe should be enough, then committing updated file.

…vider-support

# Conflicts:
#	pnpm-lock.yaml

Co-authored-by: panz3r <1754457+panz3r@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot please resolve conflicts in lock-file.

Running pnpm i and pnpm dedupe should be enough, then committing updated file.

Done in 03f8bdc. Merged origin/main into the branch, took main's lockfile as the resolution base, then ran pnpm install --no-frozen-lockfile + pnpm dedupe to incorporate our lib/package.json changes. No conflict markers remain and all 46 tests pass.

Copilot AI requested a review from panz3r March 28, 2026 15:36
@panz3r panz3r merged commit 581697d into main Mar 28, 2026
17 checks passed
@panz3r panz3r deleted the copilot/add-multi-provider-support branch March 28, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants