-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from superglue-ai/hosted-prep
added org id to datamodel, auth providers, and datastore tests
- Loading branch information
Showing
24 changed files
with
888 additions
and
380 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,6 @@ node_modules | |
**/.turbo | ||
.DS_Store | ||
**/.next | ||
build.sh | ||
build.sh | ||
diff.txt | ||
diff.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.