Skip to content

Commit c8ccc49

Browse files
authored
fix: remove hardcoded horizon URL and dynamic URL generation (#21)
* refactor: remove hardcoded horizon URL and implement dynamic URL generation * fix: validate organization ID format in public key extraction * refactor: rename horizonServerUrls to horizonUrls
1 parent 7028be0 commit c8ccc49

File tree

5 files changed

+26
-19
lines changed

5 files changed

+26
-19
lines changed

src/config.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
export const horizon = {
2-
url: `https://horizon.hyphen.ai`,
3-
};
4-
51
export const cache = {
62
ttlSeconds: process.env.CACHE_TTL_SECONDS ? parseInt(process.env.CACHE_TTL_SECONDS) : 30,
73
};

src/hyphenClient.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import type { EvaluationResponse, HyphenEvaluationContext, HyphenProviderOptions, TelemetryPayload } from './types';
2-
import { horizon } from './config';
32
import type { Logger } from '@openfeature/server-sdk';
43
import { CacheClient } from './cacheClient';
4+
import { buildDefaultHorizonUrl } from './utils';
55

66
export class HyphenClient {
77
private readonly publicKey: string;
8-
private readonly horizonServerUrls: string[];
8+
private readonly horizonUrls: string[];
9+
private readonly defaultHorizonUrl: string;
910
private cache: CacheClient;
1011

1112
constructor(publicKey: string, options: HyphenProviderOptions) {
1213
this.publicKey = publicKey;
13-
this.horizonServerUrls = [...(options.horizonServerUrls || []), horizon.url];
14+
this.defaultHorizonUrl = buildDefaultHorizonUrl(publicKey);
15+
this.horizonUrls = [...(options.horizonServerUrls || []), this.defaultHorizonUrl];
1416
this.cache = new CacheClient(options.cache);
1517
}
1618

1719
private async tryUrls(urlPath: string, payload: unknown, logger?: Logger): Promise<Response> {
1820
let lastError: unknown;
1921

20-
for (let url of this.horizonServerUrls) {
22+
for (let url of this.horizonUrls) {
2123
try {
2224
const baseUrl = new URL(url);
2325
const basePath = baseUrl.pathname.replace(/\/$/, '');

src/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function getOrgIdFromPublicKey(publicKey: string) {
2+
try {
3+
const keyWithoutPrefix = publicKey.replace(/^public_/, '');
4+
const decoded = Buffer.from(keyWithoutPrefix, 'base64').toString();
5+
const [orgId] = decoded.split(':');
6+
const isValidOrgId = /^[a-zA-Z0-9_-]+$/.test(orgId);
7+
return isValidOrgId ? orgId : undefined;
8+
} catch {
9+
return undefined;
10+
}
11+
}
12+
13+
export function buildDefaultHorizonUrl(publicKey: string): string {
14+
const orgId = getOrgIdFromPublicKey(publicKey);
15+
return orgId ? `https://${orgId}.toggle.hyphen.cloud` : 'https://toggle.hyphen.cloud';
16+
}

tests/config.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ describe('Config', () => {
1212
process.env = originalEnv;
1313
});
1414

15-
describe('horizon', () => {
16-
it('should use production URL when NODE_ENV is production', async () => {
17-
process.env.NODE_ENV = 'production';
18-
const { horizon } = await import('../src/config');
19-
expect(horizon.url).toBe('https://horizon.hyphen.ai');
20-
});
21-
});
22-
2315
describe('cache', () => {
2416
it('should use default TTL when CACHE_TTL_SECONDS is not set', async () => {
2517
delete process.env.CACHE_TTL_SECONDS;

tests/hyphenClient.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ vi.mock('../src/config', () => {
1616
vi.stubGlobal('fetch', vi.fn());
1717

1818
describe('HyphenClient', () => {
19-
const publicKey = 'test-public-key';
20-
const mockUrl = 'https://mock-horizon-url.com';
19+
const publicKey = 'public_b3JnLTEyMzpwcm9qZWN0OnJhbmRvbTEyMw==';
20+
const organizationId = 'org-123';
21+
const mockUrl = `https://${organizationId}.toggle.hyphen.cloud`;
2122
const mockEvaluateUrl = `${mockUrl}/toggle/evaluate`;
2223
const mockTelemetryUrl = `${mockUrl}/toggle/telemetry`;
2324
const mockContext: HyphenEvaluationContext = {
@@ -180,7 +181,7 @@ describe('HyphenClient', () => {
180181

181182
it('should add horizon URL if not present in the server URLs', () => {
182183
const client = new HyphenClient(publicKey, options);
183-
expect(client['horizonServerUrls']).toEqual([mockUrl]);
184+
expect(client['horizonUrls']).toEqual([mockUrl]);
184185
});
185186

186187
it('should handle non-successful responses and set the lastError', async () => {

0 commit comments

Comments
 (0)