JSPM

@firebase-bridge/auth-context

0.0.7
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 34
  • Score
    100M100P100Q86376F
  • License Apache-2.0

High-fidelity test utilities for Firebase Cloud Functions auth contexts. Register mock identities (Auth + App Check) and inject them into v1/v2 https.onCall and https.onRequest handlers for deterministic unit tests—no emulator required.

Package Exports

  • @firebase-bridge/auth-context

Readme

@firebase-bridge/auth-context

High-fidelity mock invocation layer for Firebase HTTPS Cloud Functions (v1 & v2), integrating a lightweight in-memory mock of the firebase-admin/auth API. Purpose-built for fast, deterministic backend unit tests without network calls or the Functions or Auth emulators.

license: Apache-2.0 node typescript

What it is

This package provides a realistic in-memory invocation harness for Firebase HTTPS Cloud Functions, compatible with both v1 (firebase-functions/v1) and v2 (firebase-functions/v2) APIs.

It executes real onCall/onRequest handlers locally and deterministically, supplying realistic:

  • auth context,
  • App Check tokens,
  • timestamps,
  • Firebase headers & metadata,
  • Express-like Request/Response objects (via node-mocks-http).

In addition, the package ships with a lightweight in-memory mock of the firebase-admin/auth API. This mock is not required to invoke Cloud Functions, but is intended for projects that:

  • inject an Auth facade in production (wrapping the real Admin SDK), and
  • want to inject a fully in-memory, emulator-free implementation in unit tests.

The mock auth API allows tests to create/update/delete users, shape claims, and exercise code paths that expect Admin SDK behaviour — all without needing the emulator or network access. It operates directly on the identities managed by each AuthManager instance. This means HTTPS function implementations invoked in tests (for example, an administrative endpoint) can indirectly call the mock auth API to:

  • enable or disable a registered identity,
  • modify a user's custom claims,
  • create or delete users,
  • or otherwise mutate authentication state

and these changes immediately affect subsequent invocations, because they update the AuthManager’s working identity set.

Important caveat — The mock Auth API includes tenantManager() and projectConfigManager() for API-shape compatibility, but these are not full implementations.
Only the following method works:

  • TenantManager.authForTenant()

All other methods on both returned types will reject with:

auth/operation-not-allowed

This prevents tests from accidentally relying on unimplemented Admin SDK features.

When to use it

Use this package for:

  • Unit and integration tests for onCall and onRequest handlers.
  • CI environments where the Functions Emulator is too slow or unavailable.
  • Deterministic tests requiring realistic auth, App Check, headers, metadata, and token generation.
  • End-to-end–style backend testing using your production codebase, with fully mocked identity state and zero emulator dependencies.

Why not the emulator (for this use case)

  • Zero boot time. Zero deploy loop. Zero external processes — just edit, save, and test
  • Deterministic token generation, identity shaping, timestamp control, and HTTP request simulation
  • Suited to tight test loops and CI pipelines where emulator startup cost and variability matter

Companion Packages

  • For a high‑fidelity in‑memory mock for the Firestore Admin SDK purpose‑built for fast, deterministic backend unit tests (no emulator boot, no deploy loop) use the companion package @firebase-bridge/firestore-admin.
  • To bind firebase-functions (v1 & v2) Firestore triggers to an in-memory Firestore database use the companion package @firebase-bridge/firestore-functions.

Used together, these packages allow you to run realistic, end-to-end–style backend tests using your production codebase — without starting the Functions or Firestore emulators.


Support

This project is made freely available under the Apache 2.0 License.
If you find it useful and would like to support ongoing development, you can buy me a coffee. ☕


Install

# npm
npm i -D @firebase-bridge/auth-context firebase-functions
# pnpm
pnpm add -D @firebase-bridge/auth-context firebase-functions
# yarn
yarn add -D @firebase-bridge/auth-context firebase-functions

Quick start

1) Construct an AuthManager

import { AuthManager, SignInProvider } from '@firebase-bridge/auth-context';

const authManager = new AuthManager({
  projectId: 'demo',
  region: 'us-central1',
});

2) Register identities

authManager.register('alice', {
  providers: SignInProvider.Google,
  email: 'alice@example.com',
});

authManager.register('anon', {
  providers: SignInProvider.anonymous(),
});

3) Invoke Https function with synthesized authentication token

Registered identities are convenience references used in tests. If key is omitted, the call is treated as unauthenticated.

await authManager.https.v2.runCallable({ key: 'alice', data: { x: 1 } }, handler);

Invoking HTTPS functions

The manager exposes:

  • authManager.https.v1.runOnCall / authManager.https.v2.runOnCall
  • authManager.https.v1.onRequest / authManager.https.v2.onRequest

These wrap real function handlers and supply a realistic context.

v2 onCall

const greet = onCall((req) => {
  return { message: `Hello, ${req.data.name}!` };
});

const result = await authManager.https.v2.runCallable(
  { key: 'alice', data: { name: 'Bob' } },
  greet
);

v1 onCall

const add = runWith({}).https.onCall((data, ctx) => {
  return { sum: data.a + data.b, caller: ctx.auth?.uid };
});

await authManager.https.v1.runCallable(
  { key: 'alice', data: { a: 2, b: 3 } },
  add
);

v2 onRequest

const hello = onRequest((req, res) => {
  res.json({ uid: (req as any).auth?.uid ?? null });
});

await authManager.https.v2.onRequest(
  { key: 'alice', options: { method: 'GET', path: '/hello' } },
  hello
);

v2 onRequest with full HTTP request shaping

The following example shows a more complete onRequest invocation using the generic AuthManager, explicit identity registration, and rich HTTP request options:

import { onRequest } from 'firebase-functions/v2/https';
import {
  AuthManager,
  SignInProvider,
  RequestHandlerV2,
} from '@firebase-bridge/auth-context';

// 1) Construct an AuthManager and register an identity
const authManager = new AuthManager({
  projectId: 'demo',
  region: 'us-central1',
});

authManager.register('john', {
  uid: 'john-uid-123',
  providers: SignInProvider.Google.override({
    email: 'john@example.com',
  }),
});

// Example handler under test
const echoHandler: RequestHandlerV2 = onRequest((req, res) => {
  const body = (req as any).body ?? {};
  const authCtx = (req as any).auth ?? {};

  res.status(200).json({
    path: req.path,
    method: req.method,
    query: req.query,
    params: req.params,
    cookies: req.cookies,
    inputValue: body.inputValue ?? null,
    uid: authCtx.uid ?? null,
  });
});

// 2) Invoke the handler with shaped HTTP options
async function invokeEcho() {
  const response = await authManager.https.v2.onRequest(
    {
      key: 'john', // use the registered identity "john"
      data: {
        // becomes req.body
        inputValue: 123,
      },
      options: {
        method: 'POST',
        url: '/api/echo?debug=true',
        cookies: {
          session: 'abc123',
        },
        originalUrl: '/root/api/echo?debug=true',
        query: {
          debug: 'true',
        },
        params: {
          widgetId: 'w-001',
        },
        headers: {
          'x-test-header': 'example',
          'content-type': 'application/json',
        },
      },
    },
    echoHandler
  );

  // node-mocks-http helpers on the response
  const status = response._getStatusCode();
  const body = response._getJSONData();

  console.log('status:', status);
  console.log('json body:', body);
}

Core Concepts

AuthManager

AuthManager is the primary entry point. It provides:

  • identity registration (register),
  • HTTPS invocation (https.v1 / https.v2),
  • context and DecodedIdToken construction (context() and token()),
  • a working identity set,
  • reset semantics (reset()),
  • a mock Admin Auth API (auth).

Registered identities

  • Added via register(key, identity).
  • Serve as convenient test references.
  • On reset(), the working set is restored from them.

Configuring identities with SignInProvider

All identities that are not anonymous must be configured with a sign-in provider via the providers field using SignInProvider sentinels:

  • SignInProvider.Google — synthetic Google identity with realistic firebase.identities entries.
  • SignInProvider.Microsoft, SignInProvider.Apple, etc. — other common providers.
  • SignInProvider.custom(id, defaults) — arbitrary provider ID with default fields.
  • SignInProvider.anonymous() — Firebase anonymous auth.

You can pass a single provider or an array of providers:

  • When multiple providers are supplied, a generated token's firebase.sign_in_provider defaults to the first provider in the providers array.
  • You can override the effective sign-in provider for a specific context/token (such as when invoking an http function) by setting the signInProvider field on AuthContextOptions / AuthTokenOptions.

For example:

authManager.register('alice', {
  providers: [
    SignInProvider.Google.override({ email: 'alice12345@gmail.com' }),
    SignInProvider.Microsoft.override({
      displayName: 'alice',
      email: 'alice12345@outlook.com',
    }),
    SignInProvider.Apple.override({
      email: 'alice12345@gmail.com',
      photoURL: 'https://photos.example.com/alice/image1.png',
    }),
  ],
});

const token = authManager.token({
  key: 'alice',
  // Specifically assert the Apple signin provider for this context to avoid defaulting to Google
  signInProvider: SignInProvider.Apple.signInProvider,
});

Generating Identity Details

Identity registration produces a persisted internal identity (equivalent to a Firebase UserRecord). All authentication token identity fields are derived only from persisted identities; providers influence identity creation at registration time only.

Identity defaults and auto-generated fields

When an identity is registered via AuthManager.register():

  • Any unspecified uid fields are auto-generated

  • Any assigned sign-in provider other than anonymous is assigned a minimal provider-specific generated identity if one is not explicitly supplied:

    • For phone providers, a synthetic E.164-like phone number is generated.
    • For all other providers, a synthetic email address is generated.
  • Provider profile fields (email, phoneNumber, displayName, photoURL, etc.) are copied into the identity’s top-level fields unless suppressProviderDefaults: true is set, or the top-level field was explicitly set.

  • Providers are processed in array order. Once a top-level field has been populated, later providers do not overwrite it.

  • If a valid email is supplied or generated and emailVerified is omitted, the identity's emailVerified field defaults to to true. This is an intentional convenience for testing and differs from Firebase’s auth api default (false).

  • creationTime, lastSignInTime, and lastRefreshTime are generated if not supplied. creationTime defaults to the current time (according to the time generator) - 1 day. If lastSignInTime is not specified, or is less than creationTime, it defaults to creationTime. If lastRefreshTime is not specified or less than lastSignInTime, it defaults to lastSignInTime.

Auth API (createUser, updateUser)

When using the mock Admin Auth API:

  • Provider defaults are not applied.
  • No values are generated or derived.
  • All identity fields must be explicitly provided (mirroring Firebase Admin SDK behaviour).

Provider identities in tokens

Provider identities are always embedded in the generated token under firebase.identities, for example:

"firebase": {
  "identities": {
    "google.com": ["<google_uid>"],
    "email": ["user@example.com"]
  }
}

These values are derived from the identity’s provider list, not from top-level fields.

Top-level token fields

Top-level identity fields like email, phoneNumber, displayName, and photoURL map to token claims such as email, phone_number, name, and photo_url. Once an identity has been created, top-level token claims are never assigned from provider fields.

Using AltKey for Identity Resolution

The key passed with an http function execution request may be:

  • undefined → unauthenticated invocation
  • string | number → lookup a registered identity by its key
  • an AltKey instance → lookup a user in the working set by UID, email, or phone

AltKey enables dynamic discovery of identities from the working identity set, including:

  • registered identities,
  • identities created via auth.createUser() or otherwise modified by the mock Admin Auth API.

Note that AltKey is not an alias system, but rather a search filter applied to a unique index.

Examples

import { AltKey } from '@firebase-bridge/auth-context';

// Lookup by UID
await authManager.https.v2.runCallable(
  { key: AltKey.uid('UID_123'), data: {} },
  handler
);

// Lookup by email (tenant optional)
await authManager.https.v2.runCallable(
  { key: AltKey.email('a@example.com'), data: {} },
  handler
);

Lookup failure rules

AltKey lookups will throw if:

  • no matching working identity is found, or
  • the matching identity is disabled.

Working identity set

This is the set used at invocation time. It includes:

  • copies of registered identities,
  • identities created via the mock Admin Auth API,
  • identities modified or deleted via the mock Admin Auth API.

Invocation fails if:

  1. no identity can be resolved for the request, or
  2. the resolved identity exists but is disabled.

Mock Admin Auth API (AuthManager.auth)

This is an optional test utility. It:

  • mimics parts of the Admin Auth SDK,
  • allows creation, update, and deletion of users,
  • mutates only the working set,
  • is not required for HTTPS invocation.

This exists primarily to support test suites that inject a facade which normally wraps the Admin Auth SDK.

Deterministic time

You may supply a custom now() function to the AuthManager constructor to stabilize token timestamps across tests.


Multi-tenant support

The mock supports multi-tenant environments. As in Firebase, each identity belongs either to the default (non-tenanted) user store or to a specific tenant’s user store. Once an identity has been created in a given store, it cannot be moved to another tenant.

Register an identity with a tenant by specifying tenantId on the IdentityConstructor passed to AuthManager.register().

Example:

authManager.register('alice', {
  providers: SignInProvider.Google,
  tenantId: 'tenant-one',
});

Create an identity for a tenant using the auth API mock.

Example:

// Obtain a tenant-scoped auth instance
const tenant = authManager.auth.tenantManager().authForTenant('tenant-two');
// Create the user
const user = await tenant.createUser({
  displayName: 'Bob',
  email: 'bob@example.com',
  emailVerified: true,
});
// Link a provider (required, otherwise request contexts will be unauthenticated)
await tenant.updateUser(user.uid, {
  providerToLink: {
    providerId: 'google.com',
    uid: '123456789',
    email: 'bob@example.com',
  },
});

When an identity belongs to a tenant, the synthesized token embeds it as:

{ "firebase": { "tenant": "tenant-one" } }

Identity Lifecycle & Reset Semantics

Working identity set mutation

The working identity set may be modified via the mock Admin Auth API, accessible through authManager.auth and its tenant-scoped instances (for example, authManager.auth.tenantManager().authForTenant(...)).

Resetting vs clearing state

authManager.reset()

  • deletes all non-registered identities,
  • restores each registered identity to its original state,
  • clears mutations introduced via the mock Admin Auth API.

authManager.clear()

  • clears all state, including registered identities,
  • returns the manager to an empty state (no identities configured).

Invocation failures

Invocation fails with a Firebase auth error if:

  • the identity does not exist in the working set,
  • the working identity is disabled.

This applies to both key-based and AltKey-based lookups.

Synthesizing tokens

AuthManager.token(options) lets you generate a DecodedIdToken directly from the same machinery used for HTTPS invocation:

const token = authManager.token({ key: 'alice' });

AuthTokenOptions extends AuthContextOptions and requires a non-undefined key (string/number or AltKey). This is useful when:

  • testing modules that operate purely on DecodedIdToken values,
  • snapshotting claims/identity shaping logic,
  • or when you want a token without invoking an HTTPS function.

Example identity configurations and resulting tokens

The snippets below illustrate how different identity configurations shape the resulting DecodedIdToken.

Minimal identity creation

authManager.register('alice', {
  providers: SignInProvider.Google,
});

const token = authManager.token({ key: 'alice' });
console.log('minimal identity creation token:', JSON.stringify(token, null, 2));

generates a token like:

{
  "sub": "vxiUXBlDQexHDstT29LChGrwOM0R",
  "aud": "demo",
  "iat": 1763704517,
  "exp": 1763706317,
  "auth_time": 1763702717,
  "uid": "vxiUXBlDQexHDstT29LChGrwOM0R",
  "iss": "https://firebaseappcheck.googleapis.com/425447859205",
  "firebase": {
    "sign_in_provider": "google.com",
    "identities": {
      "google.com": ["990058071739787504953"],
      "email": ["user-422347@example.com"]
    }
  },
  "email": "user-422347@example.com",
  "email_verified": true
}

Enhanced identity creation (MFA + custom claims)

authManager.register('alice', {
  providers: SignInProvider.Google.override({
    phoneNumber: '+5551234567',
  }),
  displayName: 'alice',
  email: 'alice@example.com',
  multiFactorEnrollments: { factorId: 'phone' },
  multiFactorDefault: 'phone',
  customClaims: {
    user_roles: ['premium-features'],
  },
  photoURL: 'https://photos.example.com/alice/image1.png',
});

const token = authManager.token({ key: 'alice' });
console.log(
  'enhanced identity creation token:',
  JSON.stringify(token, null, 2)
);

generates a token like:

{
  "sub": "9YSAX91fEcDqumXv6uGoBuHFM3kP",
  "aud": "demo",
  "iat": 1763704517,
  "exp": 1763706317,
  "auth_time": 1763702717,
  "uid": "9YSAX91fEcDqumXv6uGoBuHFM3kP",
  "iss": "https://firebaseappcheck.googleapis.com/425447859205",
  "firebase": {
    "sign_in_provider": "google.com",
    "identities": {
      "google.com": ["726351463890305018478"],
      "phone": ["+5551234567"]
    },
    "sign_in_second_factor": "phone",
    "second_factor_identifier": "5LBGdIcI5fboqVV4tlvO0Du6cmFT"
  },
  "email": "alice@example.com",
  "email_verified": true,
  "phone_number": "+5551234567",
  "photo_url": "https://photos.example.com/alice/image1.png",
  "name": "alice",
  "user_roles": ["premium-features"]
}

Multiple identity providers

authManager.register('alice', {
  providers: [
    SignInProvider.Google.override({ email: 'alice12345@gmail.com' }),
    SignInProvider.Microsoft.override({
      displayName: 'alice',
      email: 'alice12345@outlook.com',
    }),
    SignInProvider.Apple.override({
      email: 'alice12345@gmail.com',
      photoURL: 'https://photos.example.com/alice/image1.png',
    }),
  ],
});

const token = authManager.token({ key: 'alice' });
console.log(
  'multiple identity creation token:',
  JSON.stringify(token, null, 2)
);

generates a token like:

{
  "sub": "tss9G1AOWoFwMxb01TWOyzz5KAZm",
  "aud": "demo",
  "iat": 1763704517,
  "exp": 1763706317,
  "auth_time": 1763702717,
  "uid": "tss9G1AOWoFwMxb01TWOyzz5KAZm",
  "iss": "https://firebaseappcheck.googleapis.com/425447859205",
  "firebase": {
    "sign_in_provider": "google.com",
    "identities": {
      "google.com": ["134134107080242480262"],
      "email": ["alice12345@gmail.com", "alice12345@outlook.com"],
      "microsoft.com": ["6e532444bf41ee3847934679b81ceb86"],
      "apple.com": ["522566.1b37fd71b9c0b98beaf0dc207eb36e61.8079"]
    }
  },
  "email": "alice12345@gmail.com",
  "email_verified": true,
  "photo_url": "https://photos.example.com/alice/image1.png",
  "name": "alice"
}

Anonymous identity creation

authManager.register('anon', {
  providers: SignInProvider.anonymous(),
});
// also by default: auth.register('anon');

const token = authManager.token({ key: 'anon' });
console.log(
  'anonymous identity creation token:',
  JSON.stringify(token, null, 2)
);

generates a token like:

{
  "sub": "6cFPERExNGZRcc5J9TqN76ud40Z7",
  "aud": "demo",
  "iat": 1763704518,
  "exp": 1763706318,
  "auth_time": 1763702718,
  "uid": "6cFPERExNGZRcc5J9TqN76ud40Z7",
  "iss": "https://firebaseappcheck.googleapis.com/425447859205",
  "firebase": {
    "sign_in_provider": "anonymous",
    "identities": {}
  }
}

Per-call Overrides

All onCall/onRequest request descriptors support optional shaping fields:

await authManager.https.v2.runCallable(
  {
    key: 'alice',
    data: { x: 1 },
    iat: 12345,
    expires: 67890,
    headers: { 'x-test': 'yes' },
  },
  handler
);
  • iat, expires → control token timestamps
  • headers → adds raw HTTP headers
  • options → supplied for onRequest handlers

HTTP headers & JWT propagation

For both onCall and onRequest invocations, the mock synthesizes an Express-like HTTP request and automatically populates key headers to match real Cloud Functions behaviour.

Host and protocol

  • host
    • Emulator: 127.0.0.1:5001
    • Hosted-style: <region>-<project>.cloudfunctions.net
  • x-forwarded-proto
    • Emulator: "http"
    • Hosted-style: "https"

These are only set if not already present on the request descriptor.

Authorization and App Check

If an ID token (DecodedIdToken) is present in the invocation context and no authorization header is already provided:

  • authorization: Bearer <jwt>
    • The <jwt> value is a JWT-encoded form of the synthesized DecodedIdToken.

If an App Check token (DecodedAppCheckToken) is present and the App Check header is missing:

  • x-firebase-appcheck: <jwt>
    • The <jwt> value is a JWT-encoded form of the synthesized App Check token.

This applies to both onCall and onRequest flows, so any code that reads tokens from HTTP headers (e.g. authorization or x-firebase-appcheck) will see realistic values.

Content type and method

  • For onCall:
    • method defaults to POST (if not explicitly set).
    • content-type is forced to application/json.
  • For onRequest:
    • method defaults to POST if not specified.
    • content-type is inferred when missing:
      • multipart/form-data; boundary=… if files are present.
      • application/x-www-form-urlencoded for simple key=value/key=[v1,v2] bodies.
      • application/json for object/array bodies.
      • text/plain; charset=utf-8 for string bodies.

All of this shaping occurs on a mutable HttpRequestOptions instance, so you can still override headers, method, or URL explicitly in your test descriptors where needed.

Identifier generation

AuthManager automatically generates realistic identifiers for you when registering identities if values such as uid or provider-specific UIDs are not explicitly supplied. In most cases this is sufficient for tests that only care about “plausible” identity data.

For scenarios where you want explicit control or consistency across tests (for example, shared constants for test users, or stable IDs reused across multiple suites), AuthManager exposes a small helper:

authManager.idGen; // alias for the static IdGenerator class

This helper provides methods for creating Firebase-like identifiers:

// Firebase-style UID (28-char alphanumeric)
const uid = authManager.idGen.firebaseUid();

// Provider-specific UID (by type)
const googleUid = authManager.idGen.providerTypeUid('google'); // 21-digit numeric
const appleUid = authManager.idGen.providerUid('apple.com'); // composite Apple-style ID

// Project/app identifiers
const projectNumber = authManager.idGen.projectNumber(); // 12-digit numeric
const appId = authManager.idGen.appId(projectNumber); // 1:<projectNumber>:web:<hex>

You can use these values to:

  • define stable test-user constants shared across suites,
  • assign a fixed uid for the AuthManager registration key (generic TKey),
  • or reuse the same synthetic provider UIDs in fixtures and snapshots.

All identifiers are generated using Math.random() and are not cryptographically secure. They are intended exclusively for mocks, fixtures, and test environments.

Using AuthManager with an Application-Level Facade (for End-to-End Testability)

In many real-world Firebase backends, authentication is not consumed directly from Cloud Function contexts. Instead, applications wrap firebase-admin/auth inside a local facade or service layer that production code calls consistently (e.g., AppServices.auth.verifyIdToken(req)).

This section demonstrates how AuthManager can be integrated into such an architecture, allowing you to run end-to-end (or near end-to-end) backend tests against your production codebase—without the emulator, without network calls, and with no conditional logic. The same production modules that call the Admin Auth SDK in production can call the mock Admin Auth API in tests.

This pattern is optional, but extremely powerful for teams building structured Firebase backends.


Minimal Example – Injecting AuthManager into a Simple Facade

A small facade provides stable entry points for your backend code:

// services.ts
import { Auth } from 'firebase-admin/auth';

export interface AppServices {
  auth: Auth; // Admin Auth implementation
  now: () => Date; // Time source
}

export const Services: AppServices = {
  auth: {} as Auth, // populated at runtime
  now: () => new Date(),
};

Your production code calls the facade:

// api/profile.ts
import { Services } from './services';

export async function getProfile(req: Request) {
  const idToken = extractIdTokenFromRequest(req); // your own helper
  const token = await Services.auth.verifyIdToken(idToken);

  return { uid: token.uid };
}

Test Initialization Using AuthManager

import { AuthManager, SignInProvider } from '@firebase-bridge/auth-context';
import { Services } from './services';

const auth = new AuthManager({
  projectId: 'demo',
  region: 'us-central1',
  now: () => Services.now().valueOf(),
});
Services.auth = auth.auth; // inject mock Admin Auth API
Services.now = () => new Date(0); // deterministic time

auth.register('alice', { providers: SignInProvider.Google });

const res = await auth.https.v2.onRequest(
  { key: 'alice', options: { method: 'GET', path: '/profile' } },
  getProfile
);

Your production handler now runs unchanged against a fully mocked environment.


Advanced: Full Integration with a Service Registry and TestAuthManager

The following expanded pattern is suitable for larger backends with multiple services, multi-tenancy, deterministic time, and environment-specific service wiring.

Show advanced DI example

Service Registry

// service-registry.ts
export type ServiceMap = Record<string, any>;

export interface ServiceStore<TServices extends ServiceMap> {
  get<K extends keyof TServices>(key: K): TServices[K];
  optional<K extends keyof TServices>(key: K): TServices[K] | undefined;
}

export class ServiceRegistry<TServices extends ServiceMap>
  implements ServiceStore<TServices>
{
  private readonly store = new Map<
    keyof TServices,
    TServices[keyof TServices]
  >();

  set<K extends keyof TServices>(key: K, value: TServices[K]): void {
    this.store.set(key, value);
  }

  get<K extends keyof TServices>(key: K): TServices[K] {
    if (!this.store.has(key)) {
      throw new Error(`Service "${String(key)}" has not been registered.`);
    }
    return this.store.get(key) as TServices[K];
  }

  optional<K extends keyof TServices>(key: K): TServices[K] | undefined {
    return this.store.get(key) as TServices[K];
  }

  reset(): void {
    this.store.clear();
  }
}

Configuration for the Runtime Environment

import { ServiceMap, ServiceRegistry, ServiceStore } from './service-registry';

// single instance for this module
const registry: ServiceStore<ServiceMap> = new ServiceRegistry<ServiceMap>();

export function serviceRegistry<
  TServices extends ServiceMap = ServiceMap
>(): ServiceStore<TServices> {
  return registry as ServiceStore<TServices>;
}

export function registerServices<TServices extends ServiceMap>(
  services: Partial<TServices>
): void {
  for (const [key, value] of Object.entries(services)) {
    if (key && value) {
      (registry as ServiceRegistry<TServices>).set(
        key as keyof TServices,
        value
      );
    }
  }
}

export function resetServices(): void {
  (registry as ServiceRegistry<ServiceMap>).reset();
}

Facade for Production Code

// app-facade.ts
import { Auth } from 'firebase-admin/auth';
import { serviceRegistry } from './app-env';
import { ServiceStore } from './service-registry';

export interface TimeService {
  now: () => Date;
  millisNow: () => number;
}

export interface PlatformServiceMap {
  readonly auth: Auth;
  readonly time: TimeService;
}

export class AppFacade {
  private readonly _services: ServiceStore<PlatformServiceMap>;

  static readonly singleton = new AppFacade();

  private constructor() {
    this._services = serviceRegistry<PlatformServiceMap>();
  }

  get auth(): Auth {
    return this._services.get('auth');
  }

  get time(): TimeService {
    return this._services.get('time');
  }
}

TestAuthManager – Bridges AuthManager into the Facade

// test-manager.ts
import { AuthManager, SignInProvider } from '@firebase-bridge/auth-context';
import { registerServices, resetServices } from './app-env';
import { PlatformServiceMap, TimeService } from './app-facade';

export class TestTimeService implements TimeService {
  private _fn: (() => Date) | undefined;
  now(): Date {
    return this._fn?.() ?? new Date();
  }

  millisNow(): number {
    return this.now().valueOf();
  }

  set(fn?: () => Date) {
    this._fn = fn;
  }
}

export class TestAuthManager extends AuthManager<string> {
  readonly time = new TestTimeService();

  constructor(options?: any) {
    super({ ...options, now: () => this.time.millisNow() });
    this.clear();
    resetServices();
    registerServices<PlatformServiceMap>({
      time: this.time,
      auth: this.auth,
    });
  }

  override reset(): void {
    this.time.set();
    super.reset();
  }

  override clear(): void {
    super.clear();
    this.register('admin', {
      uid: 'admin',
      providers: SignInProvider.Google.override({ email: 'admin@example.com' }),
    });
  }
}

Example Test Using the Facade

const auth = new TestAuthManager({ projectId: 'demo', region: 'us-central1' });

auth.register('alice', {
  uid: 'alice',
  providers: SignInProvider.Google.override({
    email: 'alice@example.com',
    tenantId: 'tenant-one',
  }),
});

// Use the facade exactly as production code would
const tenant = AppFacade.singleton.auth
  .tenantManager()
  .authForTenant('tenant-one');

const user = await tenant.getUser('alice');
console.log(user.uid); // "alice"

Notes on fidelity

  • realistic UID/email/phone/provider data
  • realistic AppCheck tokens
  • correct Firebase headers and metadata
  • deterministic context construction

Versioning & compatibility

  • Peer dependency: firebase-functions
  • Node ≥ 18
  • Works in ESM and CJS

Contributing

Minimal-maintainer mode. Issues welcome; PRs for fixes/docs.


License

Apache-2.0 © 2025 Bryce Marshall


Trademarks

Not affiliated with Google LLC. “Firebase” and “Cloud Functions” are trademarks of Google LLC.