Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions wallet-gateway/remote/src/config/Config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import { expect, test } from 'vitest'
import { ConfigUtils } from './ConfigUtils.js'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

test('config from json file', async () => {
const resp = ConfigUtils.loadConfigFile('../test/config.json')
Expand All @@ -28,3 +31,64 @@ test('config from json file', async () => {
)
}
})

test('normalizes host-only or partial ledgerApi.baseUrl values', async () => {
const config = JSON.parse(readFileSync('../test/config.json', 'utf-8'))

const inputToExpected: Array<[string, string]> = [
['127.0.0.1', 'http://127.0.0.1:80'],
['http://127.0.0.1', 'http://127.0.0.1:80'],
['https://127.0.0.1', 'https://127.0.0.1:443'],
['127.0.0.1:5003', 'http://127.0.0.1:5003'],
]

for (const [input, expected] of inputToExpected) {
const tempConfig = structuredClone(config)
tempConfig.bootstrap.networks[0].ledgerApi.baseUrl = input

const tempDir = mkdtempSync(join(tmpdir(), 'wg-config-'))
const tempFile = join(tempDir, 'config.json')
writeFileSync(tempFile, JSON.stringify(tempConfig), 'utf-8')

const loaded = ConfigUtils.loadConfigFile(tempFile)
expect(loaded.bootstrap.networks[0].ledgerApi.baseUrl).toBe(expected)
}
})

test('normalizes host-only or partial OAuth IDP issuer values', async () => {
const config = JSON.parse(readFileSync('../test/config.json', 'utf-8'))

const inputToExpected: Array<[string, string]> = [
['127.0.0.1', 'http://127.0.0.1:80'],
['http://127.0.0.1', 'http://127.0.0.1:80'],
['https://127.0.0.1', 'https://127.0.0.1:443'],
['127.0.0.1:5003', 'http://127.0.0.1:5003'],
]

for (const [input, expected] of inputToExpected) {
const tempConfig = structuredClone(config)
tempConfig.bootstrap.idps[0].issuer = input

const tempDir = mkdtempSync(join(tmpdir(), 'wg-config-'))
const tempFile = join(tempDir, 'config.json')
writeFileSync(tempFile, JSON.stringify(tempConfig), 'utf-8')

const loaded = ConfigUtils.loadConfigFile(tempFile)
expect(loaded.bootstrap.idps[0].issuer).toBe(expected)
}
})

test('keeps non-url self-signed IDP issuer unchanged', async () => {
const config = JSON.parse(readFileSync('../test/config.json', 'utf-8'))
const expectedIssuer = 'unsafe-auth'

const tempConfig = structuredClone(config)
tempConfig.bootstrap.idps[2].issuer = expectedIssuer

const tempDir = mkdtempSync(join(tmpdir(), 'wg-config-'))
const tempFile = join(tempDir, 'config.json')
writeFileSync(tempFile, JSON.stringify(tempConfig), 'utf-8')

const loaded = ConfigUtils.loadConfigFile(tempFile)
expect(loaded.bootstrap.idps[2].issuer).toBe(expectedIssuer)
})
104 changes: 101 additions & 3 deletions wallet-gateway/remote/src/config/ConfigUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { Env } from '../env.js'
export class ConfigUtils {
static loadConfigFile(filePath: string): Config {
if (existsSync(filePath)) {
const rawConfig = rawConfigSchema.parse(
JSON.parse(readFileSync(filePath, 'utf-8'))
)
const rawJson = JSON.parse(readFileSync(filePath, 'utf-8'))
const normalizedJson = normalizeConfigUrls(rawJson)

const rawConfig = rawConfigSchema.parse(normalizedJson)

const config = resolveRawConfig(rawConfig)

Expand Down Expand Up @@ -62,6 +63,103 @@ export class ConfigUtils {
}
}

function normalizeConfigUrls(input: unknown): unknown {
if (!input || typeof input !== 'object') {
return input
}

const candidate = input as {
bootstrap?: {
idps?: Array<{ issuer?: string }>
networks?: Array<{ ledgerApi?: { baseUrl?: string } }>
}
}

const idps = candidate.bootstrap?.idps
if (Array.isArray(idps)) {
for (const idp of idps) {
const issuer = idp.issuer
if (typeof issuer === 'string' && shouldNormalizeAsUrl(issuer)) {
idp.issuer = normalizeBaseUrl(issuer)
}
}
}

const networks = candidate.bootstrap?.networks
if (Array.isArray(networks)) {
for (const network of networks) {
const baseUrl = network.ledgerApi?.baseUrl
if (typeof baseUrl === 'string') {
network.ledgerApi!.baseUrl = normalizeBaseUrl(baseUrl)
}
}
}

return input
}

function shouldNormalizeAsUrl(value: string): boolean {
const trimmed = value.trim()

// Preserve non-URL issuers for self-signed setups, e.g. "unsafe-auth".
if (
!trimmed.includes('://') &&
!trimmed.includes('.') &&
!trimmed.includes(':')
) {
return trimmed.toLowerCase() === 'localhost'
}

return true
}

function normalizeBaseUrl(baseUrl: string): string {
const trimmed = baseUrl.trim()

if (trimmed.length === 0) {
return baseUrl
}

if (trimmed.includes('://')) {
return normalizeUrlWithProtocol(new URL(trimmed))
}

const withDefaultProtocol = new URL(`http://${trimmed}`)
const hasExplicitPort = hasPortInAuthority(trimmed)
if (!hasExplicitPort) {
withDefaultProtocol.port = '80'
}

return normalizeUrlWithProtocol(withDefaultProtocol)
}

function hasPortInAuthority(rawValue: string): boolean {
const authority = rawValue.split(/[/?#]/)[0]

// IPv6 literal with explicit port, e.g. [::1]:5003
if (authority.startsWith('[')) {
return authority.includes(']:')
}

return authority.includes(':')
}

function normalizeUrlWithProtocol(url: URL): string {
const defaultPort =
url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : ''
const port = url.port || defaultPort
const hostname = url.hostname.includes(':')
? `[${url.hostname}]`
: url.hostname
const authority = port ? `${hostname}:${port}` : hostname
const path =
url.pathname === '/' && !url.search && !url.hash
? ''
: `${url.pathname}${url.search}${url.hash}`

return `${url.protocol}//${authority}${path}`
}

type RawNetworkAuth = NonNullable<
RawConfig['bootstrap']['networks'][number]['adminAuth']
>
Expand Down
Loading