Skip to content

Commit

Permalink
Merge pull request #8 from superglue-ai/hosted-prep
Browse files Browse the repository at this point in the history
added org id to datamodel, auth providers, and datastore tests
  • Loading branch information
stefanfaistenauer authored Feb 9, 2025
2 parents c3c83f7 + e594dd0 commit e221684
Show file tree
Hide file tree
Showing 24 changed files with 888 additions and 380 deletions.
16 changes: 13 additions & 3 deletions .github/workflows/node-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ on:
jobs:
test:
runs-on: ubuntu-latest

env:
VITE_REDIS_HOST: ${{ secrets.REDIS_HOST }}
VITE_REDIS_PORT: ${{ secrets.REDIS_PORT }}
VITE_REDIS_USERNAME: ${{ secrets.REDIS_USERNAME }}
VITE_REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}

steps:
- uses: actions/checkout@v3

Expand All @@ -17,9 +22,14 @@ jobs:
with:
node-version: '20.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm run test
run: |
VITE_REDIS_HOST=$VITE_REDIS_HOST \
VITE_REDIS_PORT=$VITE_REDIS_PORT \
VITE_REDIS_USERNAME=$VITE_REDIS_USERNAME \
VITE_REDIS_PASSWORD=$VITE_REDIS_PASSWORD \
npm run test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ node_modules
**/.turbo
.DS_Store
**/.next
build.sh
build.sh
diff.txt
diff.sh
6 changes: 6 additions & 0 deletions packages/core/auth/apiKeyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

export interface ApiKeyManager {
getApiKeys(): Promise<{ orgId: string; key: string }[]>;
authenticate(apiKey: string): Promise<{ orgId: string; success: boolean }>;
cleanup(): void;
}
31 changes: 31 additions & 0 deletions packages/core/auth/localKeyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiKeyManager } from "./apiKeyManager.js";

export class LocalKeyManager implements ApiKeyManager {
private readonly authToken: string | undefined;
private readonly defaultOrgId = "";

constructor() {
this.authToken = process.env.AUTH_TOKEN;
}

public async getApiKeys(): Promise<{ orgId: string; key: string }[]> {
if (!this.authToken) {
return [];
}
return [{ orgId: this.defaultOrgId, key: this.authToken }];
}

public async authenticate(apiKey: string): Promise<{ orgId: string; success: boolean }> {
if (!this.authToken) {
return { orgId: '', success: false };
}
return {
orgId: this.defaultOrgId,
success: apiKey === this.authToken
};
}

public cleanup(): void {
// No cleanup needed for local key manager
}
}
101 changes: 101 additions & 0 deletions packages/core/auth/supabaseKeyManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { expect, it, describe, beforeEach, afterEach, vi } from 'vitest';
import { SupabaseKeyManager } from './supabaseKeyManager.js';

describe('SupabaseKeyManager', () => {
const mockFetch = vi.fn();
const originalFetch = global.fetch;
const mockEnv = {
NEXT_PUBLIC_SUPABASE_URL: 'http://test.com',
NEXT_PUBLIC_SUPABASE_ANON_KEY: 'test-anon-key',
NEXT_PUBLIC_PRIV_SUPABASE_SERVICE_ROLE_KEY: 'test-service-key'
};
let keyManager: SupabaseKeyManager;

beforeEach(() => {
vi.stubGlobal('fetch', mockFetch);
process.env = { ...mockEnv };
keyManager = new SupabaseKeyManager();
});

afterEach(() => {
vi.stubGlobal('fetch', originalFetch);
vi.clearAllMocks();
keyManager.cleanup();
});

it('should return empty array when no keys found', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => []
});

const result = await keyManager.getApiKeys();
expect(result).toEqual([]);
});

it('should return filtered active keys', async () => {
const mockData = [
{ org_id: '1', key: 'key1', is_active: true },
{ org_id: '2', key: 'key2', is_active: false },
{ org_id: '3', key: 'key3', is_active: true }
];

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData
});

const result = await keyManager.getApiKeys();
expect(result).toEqual([
{ orgId: '1', key: 'key1' },
{ orgId: '3', key: 'key3' }
]);
});

it('should return empty array and log error when environment variables are missing', async () => {
process.env = {};
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

const result = await keyManager.getApiKeys();

expect(result).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith('Missing required Supabase environment variables');

consoleSpy.mockRestore();
});

it('should return empty array when fetch fails', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
statusText: 'Not Found'
});

const result = await keyManager.getApiKeys();
expect(result).toEqual([]);
});

it('should cache results and not fetch again within TTL', async () => {
const mockData = [
{ org_id: '1', key: 'key1', is_active: true }
];

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData
});

// First call should fetch
await keyManager.getApiKeys();
// Second call should use cache
await keyManager.getApiKeys();
// Third call should use cache
await keyManager.getApiKeys();
// Fourth call should use cache
await keyManager.getApiKeys();

// one call plus interval call
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});


82 changes: 82 additions & 0 deletions packages/core/auth/supabaseKeyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ApiKeyManager } from "./apiKeyManager.js";

export class SupabaseKeyManager implements ApiKeyManager {
private cachedApiKeys: { key: string; orgId: string }[] = [];
private lastFetchTime = 0;
private readonly API_KEY_CACHE_TTL = 60000; // 1 minute cache
private refreshInterval: NodeJS.Timeout;

constructor() {
this.refreshApiKeys();
this.refreshInterval = setInterval(
() => this.refreshApiKeys(),
this.API_KEY_CACHE_TTL
);
}

public async getApiKeys(): Promise<{ orgId: string; key: string }[]> {
// Check cache first
if (this.cachedApiKeys.length > 0) {
return this.cachedApiKeys;
}

// If cache is empty or expired, refresh the keys
await this.refreshApiKeys();
return this.cachedApiKeys;
}

public async authenticate(apiKey: string): Promise<{ orgId: string; success: boolean }> {
const keys = await this.getApiKeys();
const key = keys.find(k => k.key === apiKey);
return { orgId: key?.orgId || '', success: !!key };
}

private async fetchApiKeys(): Promise<{ orgId: string; key: string }[]> {
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const SUPABASE_SERVICE_ROLE_KEY = process.env.NEXT_PUBLIC_PRIV_SUPABASE_SERVICE_ROLE_KEY;

if (!SUPABASE_URL || !SUPABASE_ANON_KEY || !SUPABASE_SERVICE_ROLE_KEY) {
console.error('Missing required Supabase environment variables');
throw new Error('Missing required Supabase environment variables');
}

const url = `${SUPABASE_URL}/rest/v1/sg_superglue_api_keys`;
console.log('Fetching API keys from:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
},
});

if (!response.ok) {
console.error('Failed to fetch API keys:', response.statusText);
return [];
}

const data = await response.json();
if (!Array.isArray(data) || data.length === 0) {
return [];
}

return data.filter(item => item.is_active === true).map(item => ({orgId: item.org_id, key: item.key}));
}

private async refreshApiKeys(): Promise<void> {
try {
if (Date.now() - this.lastFetchTime < this.API_KEY_CACHE_TTL) {
return;
}
this.cachedApiKeys = await this.fetchApiKeys();
this.lastFetchTime = Date.now();
} catch (error) {
console.error('Failed to refresh API keys:', error);
}
}

public cleanup(): void {
clearInterval(this.refreshInterval);
}
}
Loading

0 comments on commit e221684

Please sign in to comment.