TypeScript SDK

Complete guide to using the HOLE Foundation API TypeScript SDK.

Installation

$npm install @hole-foundation/api

Or with pnpm:

$pnpm add @hole-foundation/api

Quick Start

1import { HoleFoundationClient } from '@hole-foundation/api';
2
3const client = new HoleFoundationClient({
4 environment: 'https://api.theholefoundation.org',
5 token: process.env.AUTH0_TOKEN
6});
7
8// Make your first API call
9const health = await client.health.check();
10console.log(`API Status: ${health.status}`);

Client Configuration

Basic Configuration

1const client = new HoleFoundationClient({
2 environment: 'https://api.theholefoundation.org',
3 token: 'your-jwt-token'
4});

Advanced Configuration

1import { HoleFoundationClient } from '@hole-foundation/api';
2
3const client = new HoleFoundationClient({
4 environment: 'https://api.theholefoundation.org',
5 token: process.env.AUTH0_TOKEN,
6
7 // Custom timeout (default: 60 seconds)
8 timeoutInSeconds: 30,
9
10 // Retry configuration (default: 2)
11 maxRetries: 3,
12
13 // Additional headers
14 headers: {
15 'X-Client-Version': '1.0.0',
16 'X-Request-ID': () => crypto.randomUUID()
17 },
18
19 // Custom fetch implementation
20 fetch: customFetch,
21
22 // Logging configuration
23 logging: {
24 level: 'debug',
25 logger: console
26 }
27});

For automatic token refresh:

1import { HoleFoundationClient } from '@hole-foundation/api';
2
3class TokenManager {
4 private token: string | null = null;
5 private expiresAt: number = 0;
6
7 async getToken(): Promise<string> {
8 if (this.token && Date.now() < this.expiresAt) {
9 return this.token;
10 }
11
12 const response = await fetch('https://dev-4fszoklachwdh46m.us.auth0.com/oauth/token', {
13 method: 'POST',
14 headers: { 'Content-Type': 'application/json' },
15 body: JSON.stringify({
16 grant_type: 'client_credentials',
17 client_id: process.env.AUTH0_M2M_CLIENT_ID,
18 client_secret: process.env.AUTH0_M2M_CLIENT_SECRET,
19 audience: 'https://api.theholefoundation.org'
20 })
21 });
22
23 const { access_token, expires_in } = await response.json();
24 this.token = access_token;
25 this.expiresAt = Date.now() + (expires_in * 1000) - 60000;
26
27 return this.token;
28 }
29}
30
31const tokenManager = new TokenManager();
32
33const client = new HoleFoundationClient({
34 environment: 'https://api.theholefoundation.org',
35 token: async () => await tokenManager.getToken()
36});

API Methods

Health

1// Check API health
2const health = await client.health.check();
3console.log(health.status); // "healthy"
4console.log(health.database); // "connected"
5console.log(health.version); // "2.0.0"
1// Search legal documents
2const results = await client.vectorSearch.search({
3 query: "FOIA exemptions for national security",
4 limit: 10,
5 includeContext: true,
6 filters: {
7 jurisdiction: "federal",
8 documentType: "statute"
9 }
10});
11
12// Access results
13results.results?.forEach(result => {
14 console.log(result.title);
15 console.log(result.score);
16 console.log(result.excerpt);
17});
18
19// Get specific document
20const document = await client.vectorSearch.getDocument('doc-123');
21console.log(document.content);

Transparency

1// List jurisdictions
2const jurisdictions = await client.transparency.listJurisdictions({
3 limit: 50,
4 type: 'state' // 'federal', 'state', or 'all'
5});
6
7// Search rights of access
8const rights = await client.transparency.searchRights({
9 query: "public records",
10 jurisdiction: "california",
11 limit: 20
12});
13
14// Search exemptions
15const exemptions = await client.transparency.searchExemptions({
16 query: "privacy",
17 jurisdiction: "federal",
18 category: "personal_information"
19});
20
21// Get statute text
22const statute = await client.transparency.getStatute('5-USC-552');
23console.log(statute.fullText);

FOIA Dashboard

1// Create FOIA request (requires authentication)
2const request = await client.foiaDashboard.createRequest({
3 jurisdiction: 'federal',
4 agency: 'Department of Justice',
5 subject: 'Freedom of Information Request',
6 description: 'Request for records regarding...',
7 requesterName: 'John Doe',
8 requesterEmail: 'john@example.com',
9 deliveryMethod: 'email'
10});
11
12// List your requests
13const myRequests = await client.foiaDashboard.listRequests({
14 status: 'pending',
15 limit: 10
16});
17
18// Get request details
19const requestDetails = await client.foiaDashboard.getRequest(request.id);
20
21// Update request
22const updated = await client.foiaDashboard.updateRequest(request.id, {
23 status: 'submitted',
24 notes: 'Request submitted to agency'
25});

Chat

1// Non-streaming chat
2const response = await client.chat.sendMessage({
3 message: "What is the Freedom of Information Act?",
4 stream: false
5});
6console.log(response.message);
7
8// Streaming chat
9const streamResponse = await client.chat.sendMessage({
10 message: "Explain FOIA exemption (b)(6)",
11 stream: true
12});
13
14// Handle stream (Server-Sent Events)
15const reader = streamResponse.body?.getReader();
16const decoder = new TextDecoder();
17
18while (true) {
19 const { done, value } = await reader.read();
20 if (done) break;
21
22 const chunk = decoder.decode(value);
23 process.stdout.write(chunk);
24}

Error Handling

1import { TheholetruthApiError, TheholetruthApiTimeoutError } from '@hole-foundation/api';
2
3try {
4 const results = await client.vectorSearch.search({
5 query: "test"
6 });
7} catch (error) {
8 if (error instanceof TheholetruthApiError) {
9 console.error('API Error:', error.statusCode);
10 console.error('Message:', error.message);
11 console.error('Body:', error.body);
12 } else if (error instanceof TheholetruthApiTimeoutError) {
13 console.error('Request timed out');
14 } else {
15 console.error('Unknown error:', error);
16 }
17}

TypeScript Types

The SDK is fully typed. Import types as needed:

1import type {
2 ChatResponse,
3 SearchRequest,
4 SearchResponse,
5 Jurisdiction,
6 RightOfAccess,
7 Exemption,
8 FoiaRequest
9} from '@hole-foundation/api';
10
11// Use types for type safety
12const searchRequest: SearchRequest = {
13 query: "FOIA",
14 limit: 10,
15 includeContext: true
16};
17
18const handleResults = (response: SearchResponse) => {
19 response.results?.forEach(result => {
20 console.log(result.title);
21 });
22};

Framework Integration

Next.js

1// app/api/search/route.ts
2import { HoleFoundationClient } from '@hole-foundation/api';
3import { NextRequest, NextResponse } from 'next/server';
4
5export async function POST(request: NextRequest) {
6 const { query } = await request.json();
7
8 const client = new HoleFoundationClient({
9 environment: process.env.HOLE_API_URL!,
10 token: process.env.AUTH0_M2M_TOKEN!
11 });
12
13 const results = await client.vectorSearch.search({ query });
14
15 return NextResponse.json(results);
16}

Express.js

1// server.ts
2import express from 'express';
3import { HoleFoundationClient } from '@hole-foundation/api';
4
5const app = express();
6const client = new HoleFoundationClient({
7 environment: process.env.HOLE_API_URL!,
8 token: process.env.AUTH0_M2M_TOKEN!
9});
10
11app.post('/api/search', async (req, res) => {
12 const { query } = req.body;
13 const results = await client.vectorSearch.search({ query });
14 res.json(results);
15});
16
17app.listen(3000);

React (Client-side)

1// hooks/useHoleApi.ts
2import { HoleFoundationClient } from '@hole-foundation/api';
3import { useAuth0 } from '@auth0/auth0-react';
4import { useMemo } from 'react';
5
6export function useHoleApi() {
7 const { getAccessTokenSilently } = useAuth0();
8
9 return useMemo(
10 () => new HoleFoundationClient({
11 environment: 'https://api.theholefoundation.org',
12 token: async () => {
13 return await getAccessTokenSilently({
14 audience: 'https://api.theholefoundation.org'
15 });
16 }
17 }),
18 [getAccessTokenSilently]
19 );
20}
21
22// components/Search.tsx
23import { useHoleApi } from '../hooks/useHoleApi';
24
25export function Search() {
26 const client = useHoleApi();
27
28 const handleSearch = async (query: string) => {
29 const results = await client.vectorSearch.search({ query });
30 setResults(results.results);
31 };
32
33 return <SearchUI onSearch={handleSearch} />;
34}

Environment Variables

Create a .env file:

$# Auth0 Configuration
$AUTH0_DOMAIN=dev-4fszoklachwdh46m.us.auth0.com
$AUTH0_M2M_CLIENT_ID=your-client-id
$AUTH0_M2M_CLIENT_SECRET=your-client-secret
$AUTH0_AUDIENCE=https://api.theholefoundation.org
$
$# API Configuration
$HOLE_API_URL=https://api.theholefoundation.org

Load with:

1import 'dotenv/config';

Testing

1// test/api.test.ts
2import { HoleFoundationClient } from '@hole-foundation/api';
3import { describe, it, expect } from 'vitest';
4
5describe('HOLE Foundation API', () => {
6 const client = new HoleFoundationClient({
7 environment: 'https://api.theholefoundation.org',
8 token: process.env.AUTH0_TOKEN!
9 });
10
11 it('should check health', async () => {
12 const health = await client.health.check();
13 expect(health.status).toBe('healthy');
14 });
15
16 it('should search documents', async () => {
17 const results = await client.vectorSearch.search({
18 query: 'FOIA',
19 limit: 5
20 });
21 expect(results.results).toBeDefined();
22 expect(results.results!.length).toBeGreaterThan(0);
23 });
24
25 it('should list jurisdictions', async () => {
26 const jurisdictions = await client.transparency.listJurisdictions();
27 expect(jurisdictions.length).toBe(52);
28 });
29});

Best Practices

1. Singleton Pattern

Create one client instance and reuse it:

1// lib/api-client.ts
2import { HoleFoundationClient } from '@hole-foundation/api';
3
4export const apiClient = new HoleFoundationClient({
5 environment: process.env.HOLE_API_URL!,
6 token: process.env.AUTH0_TOKEN!
7});
8
9// Use throughout your app
10import { apiClient } from './lib/api-client';
11const results = await apiClient.vectorSearch.search({ query: 'FOIA' });

2. Error Boundaries

Wrap API calls in try-catch:

1async function searchDocuments(query: string) {
2 try {
3 return await client.vectorSearch.search({ query });
4 } catch (error) {
5 if (error instanceof TheholetruthApiError) {
6 // Handle API errors
7 logger.error('API error', { statusCode: error.statusCode });
8 }
9 throw error;
10 }
11}

3. Request Deduplication

Use SWR or React Query for caching:

1import useSWR from 'swr';
2
3function useSearch(query: string) {
4 return useSWR(
5 ['search', query],
6 async () => {
7 return await client.vectorSearch.search({ query });
8 }
9 );
10}

Package Information

Support