diff --git a/sources/src/develocity/short-lived-token.ts b/sources/src/develocity/short-lived-token.ts index 3708887f..8a3281b8 100644 --- a/sources/src/develocity/short-lived-token.ts +++ b/sources/src/develocity/short-lived-token.ts @@ -174,3 +174,22 @@ export class DevelocityAccessCredentials { return this.accessKeyRegexp.test(allKeys) } } + +/** + * Resolve the access key that matches a given Develocity server, for use as the + * `develocityAccessToken` cache option. Returns `undefined` (fail-closed) when the access key is + * empty/malformed, the server URL is empty/unparseable, or no key matches the server's host. + */ +export function resolveAccessKeyForServer(accessKey: string, serverUrl: string): string | undefined { + const creds = DevelocityAccessCredentials.parse(accessKey) + if (!creds || !serverUrl) { + return undefined + } + let host: string + try { + host = new URL(serverUrl).hostname + } catch { + host = serverUrl // tolerate a bare hostname (no scheme) + } + return creds.keys.find(k => k.hostname === host)?.key +} diff --git a/sources/test/jest/short-lived-token.test.ts b/sources/test/jest/short-lived-token.test.ts index db9867a8..11ba0136 100644 --- a/sources/test/jest/short-lived-token.test.ts +++ b/sources/test/jest/short-lived-token.test.ts @@ -1,7 +1,7 @@ import nock from "nock"; import {describe, expect, it} from '@jest/globals' -import {DevelocityAccessCredentials, getToken} from "../../src/develocity/short-lived-token"; +import {DevelocityAccessCredentials, getToken, resolveAccessKeyForServer} from "../../src/develocity/short-lived-token"; describe('short lived tokens', () => { it('parse valid access key should return an object', async () => { @@ -134,3 +134,37 @@ describe('short lived tokens with retry', () => { .toBeNull() }) }) + +describe('resolveAccessKeyForServer', () => { + it('returns the key matching the server host from a full URL', () => { + expect(resolveAccessKeyForServer('ge.example.com=key1;other=key2', 'https://ge.example.com')).toBe('key1') + }) + + it('matches on hostname, ignoring scheme, port and path', () => { + expect(resolveAccessKeyForServer('ge.example.com=key1', 'https://ge.example.com:8443/path')).toBe('key1') + }) + + it('tolerates a bare hostname with no scheme', () => { + expect(resolveAccessKeyForServer('ge.example.com=key1', 'ge.example.com')).toBe('key1') + }) + + it('selects the matching key when multiple are present', () => { + expect(resolveAccessKeyForServer('dev=key1;ge.example.com=key2', 'https://ge.example.com')).toBe('key2') + }) + + it('returns undefined when no key matches the server host', () => { + expect(resolveAccessKeyForServer('ge.example.com=key1', 'https://other.example.com')).toBeUndefined() + }) + + it('returns undefined for an empty server URL', () => { + expect(resolveAccessKeyForServer('ge.example.com=key1', '')).toBeUndefined() + }) + + it('returns undefined for an empty access key', () => { + expect(resolveAccessKeyForServer('', 'https://ge.example.com')).toBeUndefined() + }) + + it('returns undefined for a malformed access key', () => { + expect(resolveAccessKeyForServer('not-a-valid-access-key', 'https://ge.example.com')).toBeUndefined() + }) +})