npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@loopstack/oauth-module

v0.4.4

Published

A provider-agnostic OAuth 2.0 module for the Loopstack automation framework. Provides a generic OAuth workflow, token storage, and a provider registry so any OAuth provider (Google, Microsoft, GitHub, etc.) can be plugged in.

Readme


title: OAuth Module description: Provider-agnostic OAuth 2.0 framework for Loopstack — OAuthModule, OAuthWorkflow, OAuthProviderRegistry, OAuthTokenStore, OAuthProviderInterface, BuildOAuthUrlTool, ExchangeOAuthTokenTool, OAuthPromptDocument, token storage with Redis fallback, pluggable provider interface, authorization code flow

@loopstack/oauth-module

OAuth module for the Loopstack automation framework.

A provider-agnostic OAuth 2.0 framework that handles the full authorization code flow — URL generation, code exchange, token storage, and automatic refresh. Any provider (Google, GitHub, Microsoft, etc.) can be plugged in by implementing a single interface.

When to Use

  • You need OAuth 2.0 authentication in your workflows (e.g. accessing Google Calendar, GitHub repos, or any third-party API)
  • You want a reusable sub-workflow that handles the entire auth flow (popup, code exchange, token storage) and resumes the parent workflow on completion
  • You are building a custom OAuth provider integration and need a standardized registration and token management layer
  • Use @loopstack/google-workspace-module or @loopstack/github-module directly if you only need Google or GitHub — they include their own providers and import OAuthModule internally

Installation

npm install @loopstack/oauth-module

Register the module in your NestJS module. OAuthModule is @Global(), so a single import makes its services available everywhere:

import { Module } from '@nestjs/common';
import { OAuthModule } from '@loopstack/oauth-module';

@Module({
  imports: [OAuthModule],
})
export class AppModule {}

Quick Start

Inject OAuthWorkflow into your workflow and launch it as a sub-workflow when authentication is needed:

import { BaseWorkflow, CallbackSchema, Guard, Transition, Workflow } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import { OAuthWorkflow } from '@loopstack/oauth-module';

@Workflow({
  schema: z.object({ calendarId: z.string().default('primary') }).strict(),
})
export class CalendarWorkflow extends BaseWorkflow<{ calendarId: string }, CalendarState> {
  constructor(
    private readonly calendarFetchEvents: CalendarFetchEventsTool,
    private readonly oAuth: OAuthWorkflow,
  ) {
    super();
  }

  @Transition({ to: 'calendar_fetched' })
  async fetchEvents(state: CalendarState, ctx: RunContext): Promise<CalendarState> {
    const args = ctx.args as { calendarId: string };
    const result = await this.calendarFetchEvents.call({ calendarId: args.calendarId });
    return {
      ...state,
      requiresAuthentication: result.data!.error === 'unauthorized',
      events: result.data!.events,
    };
  }

  @Transition({ from: 'calendar_fetched', to: 'awaiting_auth', priority: 10 })
  @Guard('needsAuth')
  async authRequired(state: CalendarState): Promise<CalendarState> {
    await this.oAuth.run(
      { provider: 'google', scopes: ['https://www.googleapis.com/auth/calendar.readonly'] },
      { callback: { transition: 'authCompleted' }, show: 'inline', label: 'Google authentication required' },
    );
    return state;
  }

  needsAuth(state: CalendarState): boolean {
    return !!state.requiresAuthentication;
  }

  @Transition({ from: 'awaiting_auth', to: 'start', wait: true, schema: CallbackSchema })
  async authCompleted(state: CalendarState, _payload: { workflowId: string }): Promise<CalendarState> {
    return state;
  }

  @Transition({ from: 'calendar_fetched', to: 'end' })
  async displayResults(state: CalendarState): Promise<unknown> {
    await this.documentStore.save(MarkdownDocument, {
      markdown: this.render(__dirname + '/templates/summary.md', { events: state.events }),
    });
    return {};
  }
}

show: 'inline' (the default) renders the OAuth sub-workflow as an embedded iframe in the parent's run view, so the user can complete authentication without leaving the page.

How It Works

Architecture

oauth-module (generic)              provider module (e.g. google)
┌──────────────────────────────┐    ┌──────────────────────────────┐
│  OAuthProviderRegistry       │◄───│  GoogleWorkspaceOAuthProvider│
│  OAuthTokenStore             │    │  (implements interface,      │
│  BuildOAuthUrlTool           │    │   registers on init)         │
│  ExchangeOAuthTokenTool      │    └──────────────────────────────┘
│  OAuthWorkflow               │
│  OAuthPromptDocument         │    consumer workflow
└──────────────────────────────┘    ┌──────────────────────────────┐
                                    │  uses OAuthTokenStore        │
                                    │  launches OAuthWorkflow      │
                                    │  via constructor injection   │
                                    └──────────────────────────────┘

The module is split into three layers:

  1. Provider registry — provider modules implement OAuthProviderInterface and self-register via OnModuleInit
  2. OAuth workflow — a generic workflow that builds the auth URL, shows a sign-in prompt, waits for the callback, then exchanges the code for tokens
  3. Token store — persists tokens per user per provider in Redis (falls back to in-memory if Redis is unavailable)

OAuthWorkflow State Machine

start ──► initiateOAuth ──► awaiting_auth ──► exchangeToken ──► end
              │                                     │
              │  Builds auth URL via                 │  Validates CSRF state,
              │  BuildOAuthUrlTool,                  │  exchanges code via
              │  saves OAuthPromptDocument           │  ExchangeOAuthTokenTool,
              │  with sign-in prompt                 │  stores tokens, updates
              │                                      │  document to 'success'
              ▼                                      ▼
         (waits for user to                    (callback resumes
          complete OAuth in browser)            parent workflow)

Token Lifecycle

  1. OAuthWorkflow calls BuildOAuthUrlTool to generate an auth URL with a CSRF state parameter
  2. The user completes OAuth in the browser popup
  3. The callback triggers exchangeToken, which validates the state and calls ExchangeOAuthTokenTool
  4. Tokens are stored per user per provider via OAuthTokenStore
  5. OAuthTokenStore.getValidAccessToken() automatically refreshes expired tokens using the provider's refreshToken() method
  6. Tools return { error: 'unauthorized' } when no valid token exists, triggering the workflow guard

Using Tokens in Custom Tools

Inject OAuthTokenStore to access stored tokens:

import { z } from 'zod';
import { BaseTool, Tool, ToolResult } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import { OAuthTokenStore } from '@loopstack/oauth-module';

@Tool({
  name: 'my_api_fetch',
  description: 'Fetches data from an OAuth-protected API.',
  schema: z.object({ query: z.string() }).strict(),
})
export class MyApiFetchTool extends BaseTool {
  constructor(private readonly tokenStore: OAuthTokenStore) {
    super();
  }

  protected async handle(args: { query: string }, ctx: RunContext): Promise<ToolResult> {
    const accessToken = await this.tokenStore.getValidAccessToken(ctx.userId, 'my-provider');

    if (!accessToken) {
      return { data: { error: 'unauthorized' } };
    }

    const response = await fetch('https://api.example.com/data', {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    return { data: await response.json() };
  }
}

Args Reference

OAuthWorkflow

| Arg | Type | Required | Description | | ---------- | ---------- | -------- | ---------------------------------------------------------------- | | provider | string | Yes | Provider ID (e.g. 'google', 'github') | | scopes | string[] | No | OAuth scopes to request (defaults to provider's defaultScopes) |

Run options:

| Option | Type | Description | | ---------- | -------- | ---------------------------------------------------------------------------------------- | | callback | object | { transition: string } — transition to call on the parent workflow when auth completes |

Returns: { authenticated: boolean }

Tools Reference

build_oauth_url

Builds an OAuth 2.0 authorization URL for the given provider with a CSRF state parameter.

| Arg | Type | Required | Description | | ---------- | ---------- | -------- | ----------------------- | | provider | string | Yes | Provider ID | | scopes | string[] | Yes | OAuth scopes to request |

Returns: { authUrl: string, state: string }

exchange_oauth_token

Exchanges an OAuth 2.0 authorization code for access and refresh tokens, validates the CSRF state, and stores the tokens globally for the user.

| Arg | Type | Required | Description | | --------------- | -------- | -------- | -------------------------------------- | | provider | string | Yes | Provider ID | | code | string | Yes | Authorization code from OAuth callback | | state | string | Yes | State parameter from callback | | expectedState | string | Yes | Expected state for CSRF validation |

Returns: { accessToken: string, refreshToken: string | undefined, expiresIn: number | undefined, scope: string | undefined }

Configuration

Redis (Token Storage)

OAuthTokenStore connects to Redis for persistent token storage. If Redis is unavailable, it falls back to in-memory storage.

| Env Variable | Default | Description | | ---------------- | ----------- | -------------- | | REDIS_HOST | localhost | Redis host | | REDIS_PORT | 6379 | Redis port | | REDIS_PASSWORD | — | Redis password |

Tokens with refresh tokens are stored with a 30-day TTL. Access-only tokens expire based on their expiresIn value.

Service Reference

OAuthProviderRegistry

Manages registered OAuth providers at runtime.

| Method | Description | | -------------------- | ---------------------------------------- | | register(provider) | Register a provider instance | | get(providerId) | Get a provider by ID (throws if missing) | | has(providerId) | Check if a provider is registered |

OAuthTokenStore

Stores and retrieves OAuth tokens per user and provider. Uses Redis with in-memory fallback.

| Method | Description | | -------------------------------------------- | ----------------------------------------------------------------- | | storeTokens(userId, providerId, tokens) | Store a StoredTokens object for a user/provider pair | | storeFromTokenSet(userId, providerId, set) | Store tokens from an OAuthTokenSet (auto-calculates expiry) | | getTokens(userId, providerId) | Get stored tokens (may be expired) | | getValidAccessToken(userId, providerId) | Get a valid access token, auto-refreshing if expired and possible |

Provider Interface Reference

Implement OAuthProviderInterface to add a new OAuth provider:

interface OAuthProviderInterface {
  readonly providerId: string; // Unique identifier, e.g. 'google', 'github'
  readonly defaultScopes: string[]; // Fallback scopes when none are specified

  buildAuthUrl(scopes: string[], state: string): string;
  exchangeCode(code: string): Promise<OAuthTokenSet>;
  refreshToken(refreshToken: string): Promise<OAuthTokenSet>;
}

interface OAuthTokenSet {
  accessToken: string;
  refreshToken?: string;
  expiresIn: number; // Seconds until expiry
  scope: string;
}

| Method | Purpose | | -------------- | ----------------------------------------------------------- | | buildAuthUrl | Construct the OAuth authorization URL for the user to visit | | exchangeCode | Exchange the authorization code for tokens after redirect | | refreshToken | Refresh an expired access token using the refresh token |

The provider self-registers via NestJS OnModuleInit:

@Injectable()
export class MyOAuthProvider implements OAuthProviderInterface, OnModuleInit {
  readonly providerId = 'my-provider';
  readonly defaultScopes = ['read', 'write'];

  constructor(private readonly registry: OAuthProviderRegistry) {}

  onModuleInit(): void {
    this.registry.register(this);
  }

  buildAuthUrl(scopes: string[], state: string): string {
    /* ... */
  }
  async exchangeCode(code: string): Promise<OAuthTokenSet> {
    /* ... */
  }
  async refreshToken(refreshToken: string): Promise<OAuthTokenSet> {
    /* ... */
  }
}

Wrap it in a module that imports OAuthModule:

@Module({
  imports: [OAuthModule],
  providers: [MyOAuthProvider],
  exports: [MyOAuthProvider],
})
export class MyOAuthModule {}

Existing Providers

| Provider | Module | Provider ID | | -------- | ------------------------------------ | ----------- | | Google | @loopstack/google-workspace-module | 'google' | | GitHub | @loopstack/github-module | 'github' |

Document Types

OAuthPromptDocument

Rendered by the oauth-prompt widget. Used internally by the OAuthWorkflow to show the sign-in prompt with a popup-based authentication flow.

| Field | Type | Description | | ---------- | ----------------------------------- | --------------------------- | | provider | string | Provider ID | | authUrl | string | The OAuth authorization URL | | state | string | CSRF state parameter | | status | 'pending' \| 'success' \| 'error' | Current auth status | | message | string (optional) | Status message |

Public API

Module

  • OAuthModule — global NestJS module, registers all providers/services/tools/workflows

Services

  • OAuthProviderRegistry — runtime registry for OAuth providers
  • OAuthTokenStore — token persistence with Redis/in-memory fallback

Tools

  • BuildOAuthUrlTool (build_oauth_url) — generates authorization URLs
  • ExchangeOAuthTokenTool (exchange_oauth_token) — exchanges codes for tokens

Workflows

  • OAuthWorkflow — generic OAuth 2.0 authorization code flow

Documents

  • OAuthPromptDocument — sign-in prompt rendered by oauth-prompt widget

Contracts

  • OAuthProviderInterface — interface for pluggable OAuth providers
  • OAuthTokenSet — token response shape returned by providers
  • StoredTokens — internal token storage shape (includes expiresAt)

Dependencies

| Package | Role | | ------------------- | ------------------------------------- | | @loopstack/common | Base classes, decorators, types | | @loopstack/core | Workflow engine, sub-workflow support | | ioredis | Redis client for token storage | | zod | Schema validation |

Related

About

Author: Jakob Klippel

License: MIT