Add resolveAccessKeyForServer for the project-cache trial

Resolves the Develocity access key matching a given server URL, for threading
into the new develocityAccessToken cache option (next commit). Reuses
DevelocityAccessCredentials.parse and fails closed (returns undefined) when the
access key is empty/malformed, the server URL is empty/unparseable, or no key
matches the server host. Tolerates a bare hostname with no scheme.

Extends short-lived-token.test.ts with coverage for full URLs, bare hostnames,
host-only matching (ignoring scheme/port/path), multi-key selection, and the
fail-closed cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daz DeBoer
2026-06-13 16:34:44 -06:00
parent ab6eefcd4a
commit fccb7323bf
2 changed files with 54 additions and 1 deletions
@@ -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
}
+35 -1
View File
@@ -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()
})
})