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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@blendededge/doohickey-hush-sdk

v0.0.13

Published

SDK for integration applications to interact with the hush service

Readme

Doohickey Secret Management SDK

A PostgreSQL-based secret management library designed specifically for DBOS applications. This SDK provides secure, encrypted storage of secrets directly in your PostgreSQL database using envelope encryption with libsodium.

Overview

This library eliminates the need for external secret management services by storing encrypted secrets directly in your DBOS application's PostgreSQL database. It's built to work seamlessly with DBOS transaction decorators and provides enterprise-grade features including multi-tenant isolation, comprehensive audit logging, and automatic encryption.

Key Architecture:

  • Direct PostgreSQL Storage: No external services required - all data stored in your DBOS database
  • Envelope Encryption: Two-layer security with libsodium XSalsa20-Poly1305 AEAD encryption
  • DBOS Integration: Native support for @DBOS.transaction() and @DBOS.workflow() decorators
  • Multi-tenant: Row-level security with tenant isolation for SaaS applications
  • OAuth2/OIDC: Built-in OAuth2 and OpenID Connect with PKCE support
  • Audit Trail: SOC2-compliant logging with 7-year retention for compliance

For Developers: Technical implementation details, architecture specifications, database schemas, and contribution guidelines are available in DEVELOPER.md.

Features

  • 🔒 Secure Encryption: Envelope encryption using libsodium XSalsa20-Poly1305 with rotating data encryption keys
  • 🏢 Multi-tenant: Tenant-based isolation for SaaS applications with row-level security
  • 📋 Audit Logging: SOC2-compliant audit trail with 7-year retention (2555 days)
  • ⚡ DBOS Native: Seamless integration with DBOS transaction patterns and workflows
  • 🔐 OAuth2/OIDC: Full OAuth2 and OpenID Connect support including Authorization Code (with PKCE), Client Credentials, Device Authorization, Resource Owner Password Credentials, and Generic Grant flows
  • 🔄 Token Management: Automatic token refresh, secure state management, and multi-service support
  • ✅ Type Safety: Full TypeScript support with comprehensive type definitions
  • 🗃️ Database Schema: Automatic table creation, migration support, and optimized indexing
  • 🔍 Search & Pagination: Built-in secret discovery, filtering, and management
  • ⚠️ Error Handling: Comprehensive error handling, validation, and troubleshooting guidance

Installation

npm install @blendededge/doohickey-hush-sdk

Peer Dependencies

This SDK requires the following peer dependencies:

{
  "peerDependencies": {
    "knex": ">=3.0.0 <4.0.0",
    "libsodium-wrappers": "^0.7.11",
    "openid-client": "^6.6.2",
  }
}

Note: In DBOS applications, knex is provided automatically via DBOS.knexClient. The other dependencies are included for encryption (libsodium-wrappers) and OAuth2/OIDC support (openid-client). UUIDs are generated using Node.js built-in crypto.randomUUID().

Quick Start

1. Generate Encryption Key

First, generate a root encryption key for your application:

# Generate a secure 32-byte base64-encoded key
node -e "console.log('ROOT_KEY_BASE64=' + require('crypto').randomBytes(32).toString('base64'))"

Add this to your environment variables:

ROOT_KEY_BASE64=your-generated-key-here

2. Initialize Database Schema

Set up the required database tables (run once during deployment):

import { DBOS } from '@dbos-inc/dbos-sdk';
import { PostgresSecretClient } from '@blendededge/doohickey-hush-sdk';

export class DatabaseSetup {
  @DBOS.transaction()
  static async initializeSecretTables() {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'setup', // Temporary tenant for schema setup
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await client.initializeSchema();
    console.log('Secret management tables created successfully');
  }
}

3. Basic Usage

import { DBOS } from '@dbos-inc/dbos-sdk';
import { PostgresSecretClient } from '@blendededge/doohickey-hush-sdk';

export class SecretWorkflow {
  @DBOS.transaction()
  static async saveApiKey() {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'tenant-123',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await client.initialize();
    
    // Save a secret
    const result = await client.saveSecret('stripe_api_key', 'sk_test_...', {
      description: 'Stripe API key for payments',
      tags: ['production', 'payment']
    });
    
    return result;
  }
  
  @DBOS.transaction()
  static async getApiKey(): Promise<string> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'tenant-123',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await client.initialize();
    
    // Retrieve a secret
    const secret = await client.getSecret('stripe_api_key');
    return secret.value;
  }
}

4. Using Helper Functions

For simpler usage without managing client instances:

import { DBOS } from '@dbos-inc/dbos-sdk';
import { PostgresSecretWorkflowHelper } from '@blendededge/doohickey-hush-sdk';

export class QuickSecretAccess {
  @DBOS.transaction()
  static async saveAndRetrieve() {
    const tenantId = 'tenant-456';
    
    // Save
    await PostgresSecretWorkflowHelper.saveSecret(
      DBOS.knexClient,
      tenantId,
      'database_url',
      'postgresql://user:pass@host:5432/db',
      { description: 'Production database URL' }
    );
    
    // Retrieve
    const secret = await PostgresSecretWorkflowHelper.getSecret(
      DBOS.knexClient,
      tenantId,
      'database_url'
    );
    
    return secret.value;
  }
}

DBOS Architecture Patterns

Critical: This SDK must follow DBOS architectural constraints for proper operation. Understanding these patterns is essential to avoid runtime errors.

DBOS Component Separation

DBOS enforces strict separation between different types of operations:

  • @DBOS.transaction(): Database operations only (PostgreSQL queries)
  • @DBOS.step(): External API calls only (HTTP requests, third-party services)
  • @DBOS.workflow(): Orchestrates between transactions and steps

❌ What NOT to Do

Never call transaction-decorated methods from within steps:

// ❌ WRONG - This will cause "Invalid call to a `transaction` function from within a `step`" error
export class BadExample {
  @DBOS.step()
  static async fetchDataFromAPI(): Promise<string> {
    // External API call (correct for step)
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    
    // ❌ WRONG - Cannot call transaction from step
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'tenant',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize(); // This calls @DBOS.transaction() internally!
    await client.saveSecret('api-data', data); // This will fail!
    
    return data;
  }
}

✅ Correct Patterns

Pattern 1: Use transactions for secret operations, steps for API calls

export class CorrectExample {
  // ✅ CORRECT - Workflow orchestrates between transaction and step
  @DBOS.workflow()
  static async processExternalData(): Promise<string> {
    // Step 1: Get access token from database (transaction)
    const accessToken = await CorrectExample.getAccessTokenTransaction();
    
    // Step 2: Call external API with token (step)  
    const apiData = await CorrectExample.fetchDataFromAPIStep(accessToken);
    
    // Step 3: Store result in secrets (transaction)
    await CorrectExample.saveSecretTransaction('api-result', apiData);
    
    return apiData;
  }
  
  // ✅ CORRECT - Transaction for database operations
  @DBOS.transaction() 
  static async getAccessTokenTransaction(): Promise<string> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'tenant',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    const secret = await client.getSecret('api-access-token');
    return secret.value;
  }
  
  // ✅ CORRECT - Step for external API calls
  @DBOS.step()
  static async fetchDataFromAPIStep(accessToken: string): Promise<string> {
    const response = await fetch('https://api.example.com/data', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    return await response.text();
  }
  
  // ✅ CORRECT - Transaction for database operations
  @DBOS.transaction()
  static async saveSecretTransaction(name: string, value: string): Promise<void> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'tenant', 
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    await client.saveSecret(name, value, {
      description: 'Data from external API',
      tags: ['external', 'processed']
    });
  }
}

Pattern 2: Pass data between decorators rather than calling across boundaries

export class DataFlowExample {
  @DBOS.workflow()
  static async authenticateAndCallService(): Promise<any> {
    // 1. Get credentials (transaction)
    const credentials = await DataFlowExample.getCredentialsTransaction();
    
    // 2. Authenticate with service (step) 
    const authToken = await DataFlowExample.authenticateStep(credentials);
    
    // 3. Store new token (transaction)
    await DataFlowExample.updateTokenTransaction(authToken);
    
    // 4. Call service with token (step)
    return await DataFlowExample.callServiceStep(authToken);
  }
  
  @DBOS.transaction()
  static async getCredentialsTransaction(): Promise<{clientId: string, clientSecret: string}> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'auth',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    const clientId = await client.getSecret('oauth-client-id');
    const clientSecret = await client.getSecret('oauth-client-secret');
    
    return {
      clientId: clientId.value,
      clientSecret: clientSecret.value
    };
  }
  
  @DBOS.step()
  static async authenticateStep(credentials: {clientId: string, clientSecret: string}): Promise<string> {
    const response = await fetch('https://auth.service.com/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: credentials.clientId,
        client_secret: credentials.clientSecret
      })
    });
    
    const tokenData = await response.json();
    return tokenData.access_token;
  }
  
  @DBOS.transaction()
  static async updateTokenTransaction(token: string): Promise<void> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'auth',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    await client.saveSecret('current-access-token', token, {
      description: 'Current OAuth access token',
      tags: ['oauth', 'current']
    });
  }
  
  @DBOS.step()
  static async callServiceStep(token: string): Promise<any> {
    const response = await fetch('https://api.service.com/data', {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    return await response.json();
  }
}

Key Principles

  1. Separate Concerns: Database operations in @DBOS.transaction(), external calls in @DBOS.step()
  2. Pass Data: Don't call across decorator boundaries, pass data through workflow parameters
  3. Orchestrate in Workflows: Use @DBOS.workflow() to coordinate between transactions and steps
  4. Error Handling: Each decorator type has its own retry and error handling semantics

Configuration

Configuration Interfaces

interface PostgresSecretClientConfig {
  /** Tenant ID for multi-tenancy support (required) */
  tenantId: string;
  
  /** Base64-encoded root encryption key (required) */
  rootKeyBase64: string;
  
  /** Enable audit logging (default: true) */
  auditEnabled?: boolean;
  
  /** Audit log retention in days (default: 2555 / 7 years) */
  retentionDays?: number;
  
  /** Enable row-level security (default: false) */
  enableRowLevelSecurity?: boolean;
  
  /** Optional logger for debugging and monitoring */
  logger?: Logger;
}

interface OAuthClientConfig {
  /** OIDC issuer URL (for auto-discovery) OR manual endpoints */
  issuerUrl?: string;
  
  /** Manual OAuth2 endpoints (if not using OIDC discovery) */
  authorizationEndpoint?: string;
  tokenEndpoint?: string;
  revocationEndpoint?: string;
  userinfoEndpoint?: string;
  
  /** OAuth2 client credentials */
  clientId: string;
  clientSecret: string;
  
  /** Redirect URIs for OAuth callback (required for authorization_code flow) */
  redirectUris: string[];
  
  /** OAuth scopes to request */
  scopes: string[];
  
  /** Additional OAuth2/OIDC configuration */
  responseTypes?: string[]; // default: ['code']
  grantTypes?: string[]; // default: ['authorization_code']
  
  /** 
   * Supported grant types for this client
   * - 'authorization_code': Interactive web/mobile authentication with PKCE
   * - 'client_credentials': Machine-to-machine authentication
   * - 'device_code': Smart TV/IoT device authentication
   * - 'password': Legacy username/password authentication (discouraged)
   * - 'refresh_token': Token refresh capability
   * - 'generic': Support for custom/future grant types
   */
  supportedGrantTypes?: ('authorization_code' | 'client_credentials' | 'device_code' | 'password' | 'refresh_token' | 'generic')[];
}

Configuration Examples

Google OAuth2/OIDC (Recommended)

const googleConfig: OAuthClientConfig = {
  issuerUrl: 'https://accounts.google.com',
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: `${process.env.BASE_URL}/oauth/callback/google`,
  scope: ['openid', 'profile', 'email'],
  usePkce: true,
  pkceMethod: 'S256'
};

GitHub OAuth2 (Manual Configuration)

const githubConfig: OAuthClientConfig = {
  authorizationEndpoint: 'https://github.com/login/oauth/authorize',
  tokenEndpoint: 'https://github.com/login/oauth/access_token',
  userinfoEndpoint: 'https://api.github.com/user',
  clientId: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  redirectUri: `${process.env.BASE_URL}/oauth/callback/github`,
  scope: ['user:email', 'read:user'],
  usePkce: true,
  tokenEndpointAuthMethod: 'client_secret_post'
};

Microsoft Azure AD

const microsoftConfig: OAuthClientConfig = {
  issuerUrl: 'https://login.microsoftonline.com/common/v2.0',
  clientId: process.env.MICROSOFT_CLIENT_ID!,
  clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
  redirectUri: `${process.env.BASE_URL}/oauth/callback/microsoft`,
  scope: ['openid', 'profile', 'email', 'User.Read'],
  usePkce: true
};

Environment Variables

# Required: Root encryption key (32-byte base64-encoded)
ROOT_KEY_BASE64=base64-encoded-32-byte-key

# OAuth2/OIDC Configuration (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
MICROSOFT_CLIENT_ID=your-microsoft-client-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
SLACK_CLIENT_ID=your-slack-client-id
SLACK_CLIENT_SECRET=your-slack-client-secret

# Application configuration
BASE_URL=https://yourapp.com

# DBOS provides these automatically:
# DATABASE_URL=postgresql://...
# Knex client via DBOS.knexClient

# Optional: Logging and debugging
DEBUG=doohickey:*
LOG_LEVEL=info

Key Generation

import { SecretEncryption } from '@packages/secret-shared-lib';

// Generate a new root key programmatically
const rootKey = await SecretEncryption.generateRootKey();
console.log('ROOT_KEY_BASE64=' + rootKey);

// Validate an existing key
const isValid = await SecretEncryption.validateRootKey(process.env.ROOT_KEY_BASE64!);

OAuth2/OIDC Integration

The SDK includes comprehensive OAuth2 and OpenID Connect support through the OAuthManager class:

OAuth Setup

import { DBOS } from '@dbos-inc/dbos-sdk';
import { OAuthManager } from '@blendededge/doohickey-hush-sdk';

export class OAuthSetup {
  @DBOS.transaction()
  static async initializeOAuth() {
    // Initialize OAuth manager
    await OAuthManager.initialize(DBOS.knexClient, {
      tenantId: 'oauth-tenant',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    // Configure multiple OAuth providers
    await OAuthManager.initializeAllClients({
      google: {
        issuerUrl: 'https://accounts.google.com',
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        redirectUri: 'https://yourapp.com/oauth/callback'
      },
      github: {
        authorizationEndpoint: 'https://github.com/login/oauth/authorize',
        tokenEndpoint: 'https://github.com/login/oauth/access_token',
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
        redirectUri: 'https://yourapp.com/oauth/github/callback'
      }
    });
  }
}

OAuth Flow Implementation

export class OAuthFlow {
  @DBOS.transaction()
  static async startGoogleAuth() {
    // Start OAuth flow with PKCE
    const { authUrl, state } = await OAuthManager.startAuthFlow(
      'google',
      ['profile', 'email', 'openid']
    );
    
    return { authUrl, state };
  }
  
  @DBOS.transaction()
  static async handleOAuthCallback(callbackUrl: string) {
    // Handle OAuth callback and get tokens
    const tokenData = await OAuthManager.handleCallback('google', callbackUrl);
    
    return {
      accessToken: tokenData.access_token,
      refreshToken: tokenData.refresh_token,
      expiresIn: tokenData.expires_in
    };
  }
  
  @DBOS.transaction()
  static async getValidToken(): Promise<string> {
    // Get valid access token (automatically refreshes if expired)
    return await OAuthManager.getValidAccessToken('google');
  }
}

OAuth Grant Types

The SDK supports all major OAuth2 grant types for different authentication scenarios:

1. Authorization Code Grant (Interactive Authentication)

Use Case: Web applications, mobile apps, and any scenario requiring user interaction.

Features:

  • PKCE (Proof Key for Code Exchange) enabled by default for security
  • Supports both OIDC discovery and manual endpoint configuration
  • Automatic token refresh with stored refresh tokens

Complete Setup with Express Endpoints

import { DBOS, WorkflowQueue } from '@dbos-inc/dbos-sdk';
import { OAuthManager, PostgresSecretClient } from '@blendededge/doohickey-hush-sdk';
import express from 'express';
import session from 'express-session';

export const app = express();
app.use(express.json());

// Configure session middleware
app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS in production
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

const oauthQueue = new WorkflowQueue('oauth_queue');

export class InteractiveAuth {
  @DBOS.transaction()
  static async initializeOAuthClient(): Promise<void> {
    // Initialize OAuth manager
    await OAuthManager.initialize(DBOS.knexClient, {
      tenantId: 'oauth-system',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });

    // Configure client for authorization code flow
    await OAuthManager.initializeClient('google', {
      issuerUrl: 'https://accounts.google.com',
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      redirectUris: ['https://yourapp.com/oauth/callback/google'],
      scopes: ['profile', 'email', 'openid'],
      supportedGrantTypes: ['authorization_code', 'refresh_token']
    });
  }

  @DBOS.workflow()
  static async startAuthWorkflow(userId?: string): Promise<{ authUrl: string; state: string }> {
    // Start OAuth flow
    const { authUrl, state } = await InteractiveAuth.startAuthTransaction();
    
    // Optionally store user context with state
    if (userId) {
      await InteractiveAuth.storeUserContextTransaction(state, userId);
    }
    
    return { authUrl, state };
  }

  @DBOS.transaction()
  static async startAuthTransaction(): Promise<{ authUrl: string; state: string }> {
    return await OAuthManager.startAuthFlow('google', ['profile', 'email', 'openid']);
  }

  @DBOS.transaction()
  static async storeUserContextTransaction(state: string, userId: string): Promise<void> {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'oauth-context',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    await client.saveSecret(`oauth-user-${state}`, userId, {
      description: `User context for OAuth state ${state}`,
      tags: ['oauth', 'temporary']
    });
  }

  @DBOS.workflow()
  static async handleCallbackWorkflow(callbackUrl: string, state: string): Promise<{
    userId: string | null;
    accessToken: string;
    refreshToken?: string;
    expiresAt?: number;
  }> {
    // Handle OAuth callback and exchange code for tokens
    const tokens = await InteractiveAuth.handleCallbackTransaction(callbackUrl);
    
    // Get user context if stored
    const userId = await InteractiveAuth.getUserContextTransaction(state);
    
    return {
      userId,
      accessToken: tokens.access_token!,
      refreshToken: tokens.refresh_token,
      expiresAt: tokens.expires_at
    };
  }

  @DBOS.transaction()
  static async handleCallbackTransaction(callbackUrl: string): Promise<any> {
    return await OAuthManager.handleCallback('google', callbackUrl);
  }

  @DBOS.transaction()
  static async getUserContextTransaction(state: string): Promise<string | null> {
    try {
      const client = new PostgresSecretClient(DBOS.knexClient, {
        tenantId: 'oauth-context',
        rootKeyBase64: process.env.ROOT_KEY_BASE64!
      });
      await client.initialize();
      
      const userSecret = await client.getSecret(`oauth-user-${state}`);
      const userId = userSecret.value;
      
      // Clean up temporary state
      await client.deleteSecret(`oauth-user-${state}`);
      
      return userId;
    } catch {
      return null;
    }
  }

  @DBOS.step()
  static async getUserProfileStep(accessToken: string): Promise<any> {
    // External API call to get user profile
    const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });
    
    if (!response.ok) {
      throw new Error(`Failed to get user profile: ${response.statusText}`);
    }
    
    return await response.json();
  }

  @DBOS.workflow()
  static async getUserProfileWorkflow(accessToken: string): Promise<any> {
    return await InteractiveAuth.getUserProfileStep(accessToken);
  }
}

// Express endpoints for OAuth flow
app.get('/auth/login', async (req: any, res: any): Promise<void> => {
  try {
    // Get user ID from session/request if available
    const userId = req.session?.userId || req.query.user_id;
    
    const handle = await DBOS.startWorkflow(InteractiveAuth, {queueName: oauthQueue.name})
      .startAuthWorkflow(userId);
    const result = await handle.getResult();
    
    // Store state in session for security
    req.session.oauthState = result.state;
    
    // Redirect to OAuth provider
    res.redirect(result.authUrl);
  } catch (error) {
    DBOS.logger.error(`OAuth login error: ${(error as Error).message}`);
    res.status(500).json({ error: 'Failed to start OAuth flow' });
  }
});

app.get('/oauth/callback/google', async (req: any, res: any): Promise<void> => {
  try {
    const callbackUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
    const state = req.query.state;
    
    // Verify state matches what we stored in session
    if (req.session.oauthState !== state) {
      throw new Error('Invalid OAuth state');
    }
    
    // Clear state from session
    delete req.session.oauthState;
    
    // Handle the callback
    const handle = await DBOS.startWorkflow(InteractiveAuth, {queueName: oauthQueue.name})
      .handleCallbackWorkflow(callbackUrl, state);
    const result = await handle.getResult();
    
    if (result.userId) {
      // Update user session with tokens
      req.session.userId = result.userId;
      req.session.accessToken = result.accessToken;
    }
    
    // Redirect to success page or dashboard
    res.redirect('/dashboard?login=success');
  } catch (error) {
    DBOS.logger.error(`OAuth callback error: ${(error as Error).message}`);
    res.redirect('/login?error=oauth_failed');
  }
});

app.get('/auth/profile', async (req: any, res: any): Promise<void> => {
  try {
    if (!req.session.accessToken) {
      res.status(401).json({ error: 'Not authenticated' });
      return;
    }
    
    // Get user profile using stored token
    const handle = await DBOS.startWorkflow(InteractiveAuth, {queueName: oauthQueue.name})
      .getUserProfileWorkflow(req.session.accessToken);
    const profileData = await handle.getResult();
    
    res.json(profileData);
  } catch (error) {
    DBOS.logger.error(`Profile error: ${(error as Error).message}`);
    res.status(500).json({ error: 'Failed to get profile' });
  }
});

app.get('/auth/logout', async (req: any, res: any): Promise<void> => {
  try {
    // Optional: Revoke tokens
    if (req.session.accessToken) {
      await OAuthManager.revokeTokens('google');
    }
    
    // Clear session
    req.session.destroy((err: any) => {
      if (err) {
        DBOS.logger.error(`Session destroy error: ${err.message}`);
      }
    });
    
    res.json({ message: 'Logged out successfully' });
  } catch (error) {
    DBOS.logger.error(`Logout error: ${(error as Error).message}`);
    res.status(500).json({ error: 'Logout failed' });
  }
});

async function main(): Promise<void> {
  DBOS.setConfig({
    name: 'oauth-app',
    databaseUrl: process.env.DBOS_DATABASE_URL!
  });
  
  // Initialize OAuth client on startup
  await InteractiveAuth.initializeOAuthClient();
  
  await DBOS.launch({ expressApp: app });
  const PORT = 3000;
  app.listen(PORT, () => {
    console.log(`🚀 Server is running on http://localhost:${PORT}`);
  });
}

main().catch(console.log);

Frontend Integration

// Frontend code to initiate OAuth flow
const startLogin = async (): Promise<void> => {
  // Redirect to your login endpoint
  window.location.href = '/auth/login';
};

// Check authentication status
const checkAuth = async (): Promise<any> => {
  try {
    const response = await fetch('/auth/profile');
    if (response.ok) {
      const profile = await response.json();
      console.log('User authenticated:', profile);
      return profile;
    } else {
      console.log('User not authenticated');
      return null;
    }
  } catch (error) {
    console.error('Auth check failed:', error);
    return null;
  }
};

// Logout
const logout = async (): Promise<void> => {
  try {
    await fetch('/auth/logout');
    window.location.href = '/login';
  } catch (error) {
    console.error('Logout failed:', error);
  }
};

2. Client Credentials Grant (Machine-to-Machine Authentication)

Use Case: Server-to-server communication, API access, microservices, background jobs.

Features:

  • No user interaction required
  • Perfect for automated systems and service accounts
  • Tokens are cached and automatically refreshed
export class ServiceAuth {
  @DBOS.transaction()
  static async setupClientCredentials(): Promise<void> {
    // Configure client for client credentials flow
    await OAuthManager.initializeClient('api-service', {
      tokenEndpoint: 'https://auth.service.com/oauth/token',
      clientId: process.env.SERVICE_CLIENT_ID!,
      clientSecret: process.env.SERVICE_CLIENT_SECRET!,
      redirectUris: [], // Not needed for client credentials
      scopes: ['api:read', 'api:write'],
      supportedGrantTypes: ['client_credentials']
    });
  }

  @DBOS.transaction()
  static async getServiceTokenTransaction(): Promise<string> {
    // Get client credentials token
    const tokens = await OAuthManager.getClientCredentialsToken('api-service', ['api:read', 'api:write']);
    return tokens.access_token!;
  }

  @DBOS.step()
  static async callProtectedAPIStep(accessToken: string, data: any): Promise<any> {
    // Use token for API call (external service call in step)
    const response = await fetch('https://api.service.com/data', {
      headers: { 
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      method: 'POST',
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error(`API call failed: ${response.statusText}`);
    }
    
    return await response.json();
  }

  @DBOS.workflow()
  static async processDataWorkflow(data: any): Promise<any> {
    // Get valid token (automatically handles refresh)
    const accessToken = await ServiceAuth.getServiceTokenTransaction();
    
    // Call external API with token
    const result = await ServiceAuth.callProtectedAPIStep(accessToken, data);
    
    return result;
  }
}

3. Device Authorization Grant (Device Flow)

Use Case: Smart TVs, CLI tools, IoT devices, gaming consoles, devices without browsers.

Features:

  • User enters code on a separate device (phone/computer)
  • No need for embedded browser in the device
  • Secure for devices with limited input capabilities
export class DeviceAuth {
  @DBOS.transaction()
  static async setupDeviceFlow(): Promise<void> {
    // Configure client for device authorization flow
    await OAuthManager.initializeClient('smart-tv', {
      issuerUrl: 'https://accounts.google.com',
      clientId: process.env.GOOGLE_TV_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_TV_CLIENT_SECRET!,
      redirectUris: [], // Not needed for device flow
      scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
      supportedGrantTypes: ['device_code']
    });
  }

  @DBOS.transaction()
  static async startDeviceAuthTransaction(): Promise<{
    userCode: string;
    verificationUri: string;
    verificationUriComplete?: string;
    expiresIn: number;
    deviceResponse: any;
  }> {
    // Initiate device flow
    const deviceResponse = await OAuthManager.initiateDeviceFlow('smart-tv', ['https://www.googleapis.com/auth/youtube.readonly']);
    
    return {
      userCode: deviceResponse.user_code,
      verificationUri: deviceResponse.verification_uri,
      verificationUriComplete: deviceResponse.verification_uri_complete,
      expiresIn: deviceResponse.expires_in,
      deviceResponse // Store this for polling
    };
  }

  @DBOS.transaction()
  static async pollForApprovalTransaction(deviceResponse: any): Promise<{
    success: boolean;
    pending?: boolean;
    expired?: boolean;
    accessToken?: string;
    error?: string;
  }> {
    try {
      // Poll for authorization (user must approve on another device)
      const tokens = await OAuthManager.pollDeviceToken('smart-tv', deviceResponse);
      
      return {
        success: true,
        accessToken: tokens.access_token
      };
    } catch (error) {
      const errorMessage = (error as Error).message;
      if (errorMessage.includes('authorization_pending')) {
        return { success: false, pending: true };
      } else if (errorMessage.includes('expired_token')) {
        return { success: false, expired: true };
      } else {
        return { success: false, error: errorMessage };
      }
    }
  }

  @DBOS.workflow()
  static async deviceAuthWorkflow(): Promise<any> {
    // Start device authentication
    const deviceInfo = await DeviceAuth.startDeviceAuthTransaction();
    
    // Display code to user (in real app, show on TV screen)
    DBOS.logger.info(`Go to ${deviceInfo.verificationUri} and enter code: ${deviceInfo.userCode}`);
    
    // Poll for user approval
    let attempts = 0;
    const maxAttempts = Math.floor(deviceInfo.expiresIn / 5); // Poll every 5 seconds
    
    while (attempts < maxAttempts) {
      await DBOS.sleep(5000); // Wait 5 seconds between polls
      
      const result = await DeviceAuth.pollForApprovalTransaction(deviceInfo.deviceResponse);
      
      if (result.success) {
        return { success: true, accessToken: result.accessToken };
      } else if (result.expired) {
        return { success: false, error: 'Device code expired' };
      } else if (!result.pending) {
        return { success: false, error: result.error };
      }
      
      attempts++;
    }
    
    return { success: false, error: 'Timeout waiting for user approval' };
  }
}

4. Resource Owner Password Credentials (Legacy Authentication)

Use Case: Legacy system migration, trusted first-party applications, internal tools.

⚠️ Security Warning: This grant type should only be used when other flows are not possible, as it requires handling user passwords directly.

export class LegacyAuth {
  @DBOS.transaction()
  static async setupPasswordAuth(): Promise<void> {
    // Configure client for password credentials flow
    await OAuthManager.initializeClient('legacy-system', {
      tokenEndpoint: 'https://legacy.service.com/oauth/token',
      clientId: process.env.LEGACY_CLIENT_ID!,
      clientSecret: process.env.LEGACY_CLIENT_SECRET!,
      redirectUris: [], // Not needed for password flow
      scopes: ['user:profile', 'user:data'],
      supportedGrantTypes: ['password']
    });
  }

  @DBOS.transaction()
  static async authenticateUserTransaction(username: string, password: string): Promise<{
    success: boolean;
    accessToken?: string;
    expiresAt?: number;
    error?: string;
  }> {
    try {
      // Get token using username/password
      const tokens = await OAuthManager.getPasswordCredentialsToken(
        'legacy-system', 
        username, 
        password, 
        ['user:profile', 'user:data']
      );
      
      return {
        success: true,
        accessToken: tokens.access_token,
        expiresAt: tokens.expires_at
      };
    } catch (error) {
      return {
        success: false,
        error: 'Invalid credentials'
      };
    }
  }

  @DBOS.workflow()
  static async legacyLoginWorkflow(username: string, password: string): Promise<any> {
    return await LegacyAuth.authenticateUserTransaction(username, password);
  }
}

5. Generic Grant Request (Custom/Future Grant Types)

Use Case: Custom OAuth implementations, future grant types, proprietary authentication flows.

Features:

  • Flexible parameter passing for any grant type
  • Support for non-standard OAuth extensions
  • Future-proof for new OAuth specifications
export class CustomAuth {
  @DBOS.transaction()
  static async setupCustomGrant(): Promise<void> {
    // Configure client for generic grants
    await OAuthManager.initializeClient('custom-service', {
      tokenEndpoint: 'https://custom.service.com/oauth/token',
      clientId: process.env.CUSTOM_CLIENT_ID!,
      clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
      redirectUris: [],
      scopes: ['custom:scope'],
      supportedGrantTypes: ['generic']
    });
  }

  @DBOS.transaction()
  static async tokenExchangeTransaction(userToken: string, targetAudience: string): Promise<{
    accessToken: string;
    tokenType: string;
    scope?: string;
  }> {
    // Use token exchange grant (RFC 8693)
    const tokens = await OAuthManager.executeGenericGrant('custom-service', 'urn:ietf:params:oauth:grant-type:token-exchange', {
      subject_token: userToken,
      subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
      audience: targetAudience,
      scope: 'custom:scope'
    });
    
    return {
      accessToken: tokens.access_token!,
      tokenType: tokens.token_type!,
      scope: tokens.scope
    };
  }

  @DBOS.transaction()
  static async jwtBearerGrantTransaction(jwtAssertion: string): Promise<any> {
    // Use JWT bearer grant (RFC 7523)
    return await OAuthManager.executeGenericGrant('custom-service', 'urn:ietf:params:oauth:grant-type:jwt-bearer', {
      assertion: jwtAssertion,
      scope: 'custom:scope'
    });
  }

  @DBOS.workflow()
  static async customAuthWorkflow(userToken: string, targetAudience: string): Promise<any> {
    return await CustomAuth.tokenExchangeTransaction(userToken, targetAudience);
  }
}

Grant Type Selection Guide

Choose the appropriate grant type based on your use case:

| Use Case | Grant Type | Security | User Interaction | Best For | |----------|------------|----------|------------------|----------| | Web Apps | Authorization Code + PKCE | High | Required | User-facing applications | | Mobile Apps | Authorization Code + PKCE | High | Required | Native mobile applications | | API Services | Client Credentials | High | None | Server-to-server communication | | Smart TVs/IoT | Device Authorization | High | Limited | Devices without browsers | | Legacy Migration | Password Credentials | Low | Required | Migrating from legacy auth | | Custom Flows | Generic Grant | Varies | Varies | Non-standard implementations |

Security Best Practices

  1. Always use Authorization Code + PKCE for user-facing applications
  2. Use Client Credentials for service-to-service communication
  3. Avoid Password Credentials except for legacy migration scenarios
  4. Use Device Flow for input-constrained devices
  5. Validate all tokens and implement proper error handling
  6. Store tokens securely using the SDK's encrypted storage
  7. Implement token refresh logic for long-running applications
  8. Use HTTPS for all OAuth endpoints in production
  9. Validate OAuth state parameters to prevent CSRF attacks
  10. Set appropriate session timeouts for security

API Reference

PostgresSecretClient

Constructor

new PostgresSecretClient(knexClient: Knex, config: PostgresSecretClientConfig)

Methods

Initialization

  • initialize(): Promise<void> - Initialize encryption and prepare for operations
  • initializeSchema(): Promise<void> - Create database tables (run once per database)

Secret Management

  • saveSecret(name: string, value: string, metadata?: SecretCreationMetadata): Promise<SaveSecretResponse>
  • getSecret(name: string): Promise<GenericSecret>
  • fetchSecret(id: string): Promise<GenericSecret>
  • updateSecret(name: string, updates: Partial<GenericSecret>): Promise<SaveSecretResponse>
  • updateSecretById(id: string, updates: Partial<GenericSecret>): Promise<SaveSecretResponse>
  • deleteSecret(name: string): Promise<void>
  • deleteSecretById(id: string): Promise<void>

Discovery & Search

  • getAvailableSecrets(page?: number, per_page?: number): Promise<PaginatedResponse<SecretMetadata>>
  • searchSecretsByName(name: string, page?: number, per_page?: number): Promise<PaginatedResponse<SecretMetadata>>
  • secretExists(id: string): Promise<boolean>

Maintenance

  • cleanupAuditLogs(retentionDays?: number): Promise<number> - Remove old audit entries

OAuthManager

Static Methods

Initialization

// Initialize OAuth manager with database connection
static initialize(
  knexClient: Knex, 
  config: PostgresSecretClientConfig, 
  logger?: Logger
): Promise<void>

// Initialize single OAuth client
static initializeClient(
  service: string, 
  config: OAuthClientConfig
): Promise<openidClient.Configuration>

// Initialize multiple OAuth clients
static initializeAllClients(
  oauthConfigs: Record<string, OAuthClientConfig>
): Promise<void>

OAuth Flow Management

// Start OAuth authorization flow (Authorization Code Grant)
static startAuthFlow(
  service: string, 
  scopes: string[], 
  redirectUri?: string
): Promise<{ authUrl: string; state: string }>

// Handle OAuth callback (Authorization Code Grant)
static handleCallback(
  service: string, 
  callbackUrl: string
): Promise<TokenEndpointResponse>

Client Credentials Grant

// Get access token using client credentials grant
static getClientCredentialsToken(
  service: string, 
  scopes?: string[]
): Promise<TokenEndpointResponse>

Device Authorization Grant

// Initiate device authorization flow
static initiateDeviceFlow(
  service: string, 
  scopes?: string[]
): Promise<DeviceAuthorizationResponse>

// Poll for device authorization completion
static pollDeviceToken(
  service: string, 
  deviceAuthorizationResponse: DeviceAuthorizationResponse
): Promise<TokenEndpointResponse>

Password Credentials Grant

// Get access token using username/password (legacy systems only)
static getPasswordCredentialsToken(
  service: string, 
  username: string, 
  password: string, 
  scopes?: string[]
): Promise<TokenEndpointResponse>

Generic Grant Support

// Execute custom/future grant types
static executeGenericGrant(
  service: string, 
  grantType: string, 
  parameters: Record<string, string>
): Promise<TokenEndpointResponse>

Token Management

// Get valid access token (auto-refresh if needed)
static getValidAccessToken(service: string): Promise<string>

// Manually refresh access token
static refreshAccessToken(
  service: string, 
  refreshToken: string
): Promise<TokenEndpointResponse>

// Revoke all tokens for service
static revokeTokens(service: string): Promise<void>

State Management (Internal)

// Store OAuth state with PKCE verifier
static storeOAuthState(
  stateId: string, 
  service: string, 
  codeVerifier?: string
): Promise<void>

// Retrieve OAuth state
static retrieveOAuthState(
  stateId: string
): Promise<{ service: string; codeVerifier?: string }>

OAuth Types

The SDK exports native openid-client types for OAuth token responses:

// Re-exported from openid-client for convenience
type TokenEndpointResponse = {
  access_token: string;
  token_type: string;
  expires_in?: number;
  refresh_token?: string;
  scope?: string;
  id_token?: string;
  [parameter: string]: unknown;
};

type DeviceAuthorizationResponse = {
  device_code: string;
  user_code: string;
  verification_uri: string;
  verification_uri_complete?: string;
  expires_in: number;
  interval?: number;
  [parameter: string]: unknown;
};

// SDK configuration types
interface OAuthClientConfig {
  // OIDC Discovery (optional)
  issuerUrl?: string;
  
  // Manual OAuth2 endpoints (required if issuerUrl not provided)
  authorizationEndpoint?: string;
  tokenEndpoint?: string;
  revocationEndpoint?: string;
  userinfoEndpoint?: string;
  
  // Common OAuth2/OIDC fields
  clientId: string;
  clientSecret: string;
  redirectUris: string[];
  scopes: string[];
  
  // Additional OAuth2 configuration
  responseTypes?: string[];
  grantTypes?: string[];
  
  // Supported grant types
  supportedGrantTypes?: (
    'authorization_code' | 
    'client_credentials' | 
    'refresh_token' | 
    'device_code' | 
    'password' | 
    'generic'
  )[];
}

interface Logger {
  info(message: string): void;
  warn(message: string): void;
  error(message: string): void;
}

PostgresSecretWorkflowHelper

Static helper methods for simple usage:

class PostgresSecretWorkflowHelper {
  static async getSecret(knexClient: Knex, tenantId: string, name: string): Promise<GenericSecret>
  static async saveSecret(knexClient: Knex, tenantId: string, name: string, value: string, metadata?: SecretCreationMetadata): Promise<SaveSecretResponse>
  static async updateSecret(knexClient: Knex, tenantId: string, name: string, updates: Partial<GenericSecret>): Promise<SaveSecretResponse>
  static async deleteSecret(knexClient: Knex, tenantId: string, name: string): Promise<void>
  static async getAvailableSecrets(knexClient: Knex, tenantId: string, page?: number, per_page?: number): Promise<PaginatedResponse<SecretMetadata>>
  static async searchSecretsByName(knexClient: Knex, tenantId: string, name: string, page?: number, per_page?: number): Promise<PaginatedResponse<SecretMetadata>>
  static async initializeSchema(knexClient: Knex): Promise<void>
}

Convenience Functions

// Direct exports for backward compatibility
export const getSecretFromPostgres = PostgresSecretWorkflowHelper.getSecret;
export const saveSecretToPostgres = PostgresSecretWorkflowHelper.saveSecret;
export const updateSecretInPostgres = PostgresSecretWorkflowHelper.updateSecret;
export const deleteSecretFromPostgres = PostgresSecretWorkflowHelper.deleteSecret;
export const getAvailableSecretsFromPostgres = PostgresSecretWorkflowHelper.getAvailableSecrets;
export const searchSecretsByNameFromPostgres = PostgresSecretWorkflowHelper.searchSecretsByName;

// OAuth Manager exports
export { OAuthManager } from './oauth-manager';
export type { 
  OAuthClientConfig,
  TokenData,
  OAuthState 
} from './types';

// PostgreSQL Secret Client exports
export { PostgresSecretClient } from './postgres-secret-client';
export { PostgresSecretWorkflowHelper } from './postgres-secret-client';
export type {
  PostgresSecretClientConfig,
  GenericSecret,
  SecretMetadata,
  SaveSecretResponse,
  PaginatedResponse,
  SecretCreationMetadata
} from './types';

Type Definitions

interface GenericSecret {
  id: string;
  tenant_id: string;
  name: string;
  value: string;
  auth_type: string;
  metadata?: {
    description?: string;
    tags?: string[];
    created_by?: string;
    [key: string]: any;
  };
  created_at: Date;
  updated_at: Date;
  is_deleted: boolean;
  deleted_at?: Date;
}

interface SecretMetadata {
  id: string;
  name: string;
  auth_type: string;
  created_at: Date;
  updated_at: Date;
  metadata?: Record<string, any>;
}

interface SaveSecretResponse {
  id: string;
  tenant_id: string;
  name: string;
  created_at: Date;
  updated_at: Date;
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    per_page: number;
    total: number;
    total_pages: number;
  };
}

interface SecretCreationMetadata {
  description?: string;
  tags?: string[];
  created_by?: string;
  [key: string]: any;
}

Usage Patterns

Multi-tenant SaaS Application

export class TenantSecrets {
  @DBOS.transaction()
  static async setupTenantSecrets(tenantId: string, apiKeys: Record<string, string>) {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await client.initialize();
    
    // Save multiple secrets for a tenant
    const results = [];
    for (const [name, value] of Object.entries(apiKeys)) {
      const result = await client.saveSecret(name, value, {
        description: `${name} for tenant ${tenantId}`,
        created_by: 'system',
        tags: ['api-key', 'tenant-setup']
      });
      results.push(result);
    }
    
    return results;
  }
  
  @DBOS.transaction()
  static async getTenantSecret(tenantId: string, secretName: string): Promise<string> {
    const secret = await PostgresSecretWorkflowHelper.getSecret(
      DBOS.knexClient,
      tenantId,
      secretName
    );
    return secret.value;
  }
}

Secret Rotation

export class SecretRotation {
  @DBOS.transaction()
  static async rotateApiKey(tenantId: string, secretName: string, newValue: string) {
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await client.initialize();
    
    // Update the secret with new value
    const result = await client.updateSecret(secretName, {
      value: newValue,
      metadata: {
        description: 'Rotated on ' + new Date().toISOString(),
        tags: ['rotated']
      }
    });
    
    return result;
  }
}

Batch Operations

export class BatchSecretOps {
  @DBOS.transaction()
  static async migrateSecrets(fromTenant: string, toTenant: string, secretNames: string[]) {
    const sourceClient = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: fromTenant,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    const targetClient = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: toTenant,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    await Promise.all([sourceClient.initialize(), targetClient.initialize()]);
    
    const migrated = [];
    for (const secretName of secretNames) {
      try {
        const secret = await sourceClient.getSecret(secretName);
        const result = await targetClient.saveSecret(
          secret.name,
          secret.value,
          {
            ...secret.metadata,
            description: `Migrated from ${fromTenant}`
          }
        );
        migrated.push(result);
      } catch (error) {
        console.warn(`Failed to migrate secret ${secretName}:`, error);
      }
    }
    
    return migrated;
  }
}

Advanced Usage Patterns

OAuth Integration Workflows

Multi-Provider OAuth Setup

export class MultiProviderAuth {
  @DBOS.transaction()
  static async initializeAllProviders() {
    // Initialize OAuth manager
    await OAuthManager.initialize(DBOS.knexClient, {
      tenantId: 'oauth-system',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    
    // Configure multiple providers
    const oauthConfigs = {
      google: {
        issuerUrl: 'https://accounts.google.com',
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        redirectUri: `${process.env.BASE_URL}/oauth/callback/google`,
        scope: ['profile', 'email', 'openid']
      },
      github: {
        authorizationEndpoint: 'https://github.com/login/oauth/authorize',
        tokenEndpoint: 'https://github.com/login/oauth/access_token',
        userinfoEndpoint: 'https://api.github.com/user',
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
        redirectUri: `${process.env.BASE_URL}/oauth/callback/github`,
        scope: ['user:email', 'read:user']
      },
      microsoft: {
        issuerUrl: 'https://login.microsoftonline.com/common/v2.0',
        clientId: process.env.MICROSOFT_CLIENT_ID!,
        clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
        redirectUri: `${process.env.BASE_URL}/oauth/callback/microsoft`,
        scope: ['openid', 'profile', 'email']
      },
      slack: {
        authorizationEndpoint: 'https://slack.com/oauth/v2/authorize',
        tokenEndpoint: 'https://slack.com/api/oauth.v2.access',
        clientId: process.env.SLACK_CLIENT_ID!,
        clientSecret: process.env.SLACK_CLIENT_SECRET!,
        redirectUri: `${process.env.BASE_URL}/oauth/callback/slack`,
        scope: ['users:read', 'channels:read']
      }
    };
    
    await OAuthManager.initializeAllClients(oauthConfigs);
    return Object.keys(oauthConfigs);
  }
}

OAuth Flow with User Context

export class UserOAuthFlow {
  @DBOS.transaction()
  static async initiateLogin(provider: string, userId: string) {
    // Start OAuth flow
    const { authUrl, state } = await OAuthManager.startAuthFlow(
      provider,
      provider === 'google' ? ['profile', 'email', 'openid'] :
      provider === 'github' ? ['user:email', 'read:user'] :
      ['openid', 'profile', 'email']
    );
    
    // Store user context with state (using secrets for secure storage)
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'user-oauth-context',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    await client.saveSecret(`oauth-state-${state}`, JSON.stringify({
      userId,
      provider,
      initiatedAt: new Date().toISOString()
    }), {
      description: `OAuth state for user ${userId}`,
      tags: ['oauth', 'temporary']
    });
    
    return { authUrl, state };
  }
  
  @DBOS.transaction()
  static async completeLogin(callbackUrl: string) {
    // Extract state from callback URL
    const url = new URL(callbackUrl);
    const state = url.searchParams.get('state')!;
    
    // Get user context
    const client = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: 'user-oauth-context',
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await client.initialize();
    
    const contextSecret = await client.getSecret(`oauth-state-${state}`);
    const { userId, provider } = JSON.parse(contextSecret.value);
    
    // Handle OAuth callback
    const tokenData = await OAuthManager.handleCallback(provider, callbackUrl);
    
    // Store tokens for user
    const userClient = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: `user-${userId}`,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await userClient.initialize();
    
    await userClient.saveSecret(`${provider}-tokens`, JSON.stringify(tokenData), {
      description: `${provider} OAuth tokens`,
      tags: ['oauth', 'tokens', provider]
    });
    
    // Clean up temporary state
    await client.deleteSecret(`oauth-state-${state}`);
    
    return { userId, provider, tokenData };
  }
  
  @DBOS.transaction()
  static async getUserAccessToken(userId: string, provider: string): Promise<string> {
    try {
      // Try to get valid token directly from OAuth manager
      return await OAuthManager.getValidAccessToken(provider);
    } catch {
      // Fallback to user-stored tokens
      const userClient = new PostgresSecretClient(DBOS.knexClient, {
        tenantId: `user-${userId}`,
        rootKeyBase64: process.env.ROOT_KEY_BASE64!
      });
      await userClient.initialize();
      
      const tokenSecret = await userClient.getSecret(`${provider}-tokens`);
      const tokenData = JSON.parse(tokenSecret.value);
      
      return tokenData.access_token;
    }
  }
}

Service Integration with OAuth

export class ServiceIntegration {
  @DBOS.transaction()
  static async syncWithGoogleDrive(userId: string, folderId: string) {
    // Get valid access token
    const accessToken = await UserOAuthFlow.getUserAccessToken(userId, 'google');
    
    // Use token with Google Drive API
    const response = await fetch(
      `https://www.googleapis.com/drive/v3/files?parents=${folderId}`,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    if (!response.ok) {
      throw new Error(`Google Drive API error: ${response.statusText}`);
    }
    
    const data = await response.json();
    
    // Store results as secrets for caching
    const userClient = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: `user-${userId}`,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await userClient.initialize();
    
    await userClient.saveSecret(
      `google-drive-cache-${folderId}`,
      JSON.stringify(data),
      {
        description: `Google Drive folder ${folderId} cache`,
        tags: ['cache', 'google-drive', 'temporary']
      }
    );
    
    return data;
  }
  
  @DBOS.transaction()
  static async postToSlack(userId: string, channel: string, message: string) {
    const accessToken = await UserOAuthFlow.getUserAccessToken(userId, 'slack');
    
    const response = await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        channel,
        text: message
      })
    });
    
    const result = await response.json();
    
    if (!result.ok) {
      throw new Error(`Slack API error: ${result.error}`);
    }
    
    return result;
  }
}

Token Lifecycle Management

export class TokenLifecycle {
  @DBOS.transaction()
  static async refreshAllExpiredTokens() {
    const services = ['google', 'github', 'microsoft', 'slack'];
    const results = [];
    
    for (const service of services) {
      try {
        // This will automatically refresh if expired
        const token = await OAuthManager.getValidAccessToken(service);
        results.push({ service, status: 'valid', token });
      } catch (error) {
        results.push({ service, status: 'error', error: error.message });
      }
    }
    
    return results;
  }
  
  @DBOS.transaction()
  static async revokeUserTokens(userId: string, provider?: string) {
    const userClient = new PostgresSecretClient(DBOS.knexClient, {
      tenantId: `user-${userId}`,
      rootKeyBase64: process.env.ROOT_KEY_BASE64!
    });
    await userClient.initialize();
    
    if (provider) {
      // Revoke specific provider tokens
      try {
        await OAuthManager.revokeTokens(provider);
        await userClient.deleteSecret(`${provider}-tokens`);
      } catch (error) {
        console.warn(`Failed to revoke ${provider} tokens:`, error);
      }
    } else {
      // Revoke all tokens for user
      const providers = ['google', 'github', 'microsoft', 'slack'];
      
      for (const prov of providers) {
        try {
          await OAuthManager.revokeTokens(prov);
          await userClient.deleteSecret(`${prov}-tokens`);
        } catch (error) {
          console.warn(`Failed to revoke ${prov} tokens:`, error);
        }
      }
    }
  }
}

Security Features

The SDK provides enterprise-grade security with the following features:

Encryption

  • Envelope Encryption: Two-layer encryption using libsodium XSalsa20-Poly1305
  • Individual Secret Keys: Each secret encrypted with its own unique key
  • Root Key Management: Secure master key rotation without re-encrypting secrets
  • AEAD Security: Authenticated encryption prevents tampering

Multi-tenant Security

  • Tenant Isolation: All operations scoped to specific tenant ID
  • Cross-tenant Protection: No data access across tenant boundaries
  • Audit Separation: Complete audit trail isolation per tenant
  • Row-level Security: Optional database-level tenant isolation

OAuth Security

  • PKCE Implementation: Proof Key for Code Exchange prevents authorization code interception
  • Secure Token Storage: OAuth tokens encrypted using same security as secrets
  • Automatic Refresh: Transparent token renewal with fallback handling
  • State Management: CSRF protection with secure random state generation

For detailed security architecture and implementation details, see DEVELOPER.md.

Database Integration

The SDK automatically creates and manages PostgreSQL tables:

  • secrets: Encrypted secret storage with tenant isolation and metadata support
  • secret_audit_log: Comprehensive audit trail with 7-year retention
  • Automatic Schema Setup: Call initializeSchema() once during deployment
  • Optimized Performance: Built-in indexing for fast tenant-scoped queries
  • Row-level Security: Optional database-level tenant isolation

For complete database schema and optimization details, see DEVELOPER.md.

Testing

Run the comprehensive test suite:

# Run all tests
npm test

# Run with coverage report
npm run test:coverage

# Run PostgreSQL-specific tests
npm run test:postgres

# Run OAuth-specific tests
npm run test:oauth

# Run in watch mode during development
npm run test:watch

# Integration tests (requires running PostgreSQL)
npm run test:integration

Test Coverage

The SDK includes comprehensive testing with 81%+ code coverage:

  • Unit Tests: All secret management and OAuth functionality
  • Integration Tests: End-to-end workflows with PostgreSQL
  • Security Tests: Encryption, tenant isolation, and OAuth flows
  • Error Handling: Edge cases and recovery scenarios
  • Performance Tests: Large dataset handling and optimization

For detailed testing infrastructure and development setup, see DEVELOPER.md.

Error Handling

The SDK provides comprehensive error handling:

try {
  const secret = await client.getSecret('nonexistent');
} catch (error) {
  if (error.message.includes('not found')) {
    // Handle missing secret
  } else if (error.message.includes('not initialized')) {
    // Handle uninitialized client
  } else {
    // Handle other errors
  }
}

Common error scenarios:

  • Uninitialized Client: Call initialize() before operations
  • Missing Secrets: Check existence with secretExists()
  • Tenant Isolation: Ensure correct tenant ID
  • Encryption Errors: Validate root key format
  • Database Errors: Check connection and permissions

Troubleshooting

Common Issues

1. "PostgresSecretClient not initialized"

// Always call initialize() first
await client.initialize();

2. "ROOT_KEY_BASE64 environment variable is required"

# Generate and set the root key
ROOT_KEY_BASE64=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")
export ROOT_KEY_BASE64

3. "Secret not found"

// Check if secret exists first
const exists = await client.secretExists(secretId);
if (!exists) {
  // Handle missing secret
}

4. Database connection issues

  • Ensure DBOS is properly configured with PostgreSQL
  • Check that DBOS.knexClient is available
  • Verify database permissions for table creation

5. OAuth configuration errors

// Validate OAuth configuration
const config = {
  issuerUrl: 'https://accounts.google.com', // For OIDC
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: 'https://yourapp.com/oauth/callback'
};

6. Token management issues

// Handle token refresh errors
try {
  const token = await OAuthManager.getValidAccessToken('google');
} catch (error) {
  if (error.message.includes('refresh_token')) {
    // User needs to re-authenticate
    await OAuthManager.revokeTokens('google');
    // Redirect to login
  }
}

Performance Tips

  1. Client Management: Create one client instance per tenant per workflow
  2. Batch Operations: Use DBOS transactions for multiple secret operations
  3. Pagination: Use appropriate page sizes (10-100) for large result sets
  4. Audit Cleanup: Regularly clean old audit logs with cleanupAuditLogs()
  5. Token Caching: OAuth tokens are automatically cached and refreshed

For detailed performance optimization strategies, see DEVELOPER.md.

License