Skip to content
Closed
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
87 changes: 87 additions & 0 deletions kun/src/contracts/network-approval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest'
import {
evaluateNetworkApproval,
isLocalNetworkHost,
normalizeNetworkHost
} from './network-approval.js'

const request = {
requestId: 'call-1',
host: 'API.Example.com.',
port: 443,
operation: 'read' as const
}

describe('network approval contract', () => {
it('denies none, allows full, and requests approval in approved mode', () => {
expect(evaluateNetworkApproval('none', request)).toEqual({ allowed: false, reason: 'denied-by-policy' })
expect(evaluateNetworkApproval('full', request)).toEqual({ allowed: true, reason: 'full-access' })
expect(evaluateNetworkApproval('approved', request)).toEqual({ allowed: false, reason: 'approval-required' })
})

it('matches host-port grants and separates read from mutation', () => {
const grant = {
scope: 'host-port' as const,
host: 'api.example.com',
port: 443,
operation: 'read' as const
}
expect(evaluateNetworkApproval('approved', request, [grant])).toEqual({
allowed: true,
reason: 'already-approved'
})
expect(evaluateNetworkApproval('approved', { ...request, operation: 'mutation' }, [grant]).reason)
.toBe('approval-required')
expect(evaluateNetworkApproval('approved', { ...request, operation: 'mutation' }, [{ ...grant, operation: 'mutation' }]))
.toEqual({ allowed: true, reason: 'already-approved' })
})

it('requires a matching request id for call grants and explicit localhost approval', () => {
const callGrant = {
scope: 'call' as const,
requestId: 'call-1',
host: 'api.example.com',
port: 443,
operation: 'read' as const
}
expect(evaluateNetworkApproval('approved', request, [callGrant]).allowed).toBe(true)
expect(evaluateNetworkApproval('approved', { ...request, requestId: 'call-2' }, [callGrant]).allowed).toBe(false)

const localhost = { ...request, host: 'localhost', port: 3000 }
const grant = { scope: 'host-port' as const, host: 'localhost', port: 3000, operation: 'read' as const }
expect(evaluateNetworkApproval('approved', localhost, [grant]).reason).toBe('approval-required')
expect(evaluateNetworkApproval('approved', localhost, [{ ...grant, allowLocalhost: true }]).allowed).toBe(true)
})

it('normalizes safe hosts and fails closed for malformed requests or grants', () => {
expect(normalizeNetworkHost(' Example.COM. ')).toBe('example.com')
expect(normalizeNetworkHost('https://example.com')).toBeNull()
expect(isLocalNetworkHost('127.0.0.1')).toBe(true)
expect(isLocalNetworkHost('example.com')).toBe(false)
expect(evaluateNetworkApproval('approved', { ...request, port: 0 })).toEqual({
allowed: false,
reason: 'invalid-request'
})
expect(evaluateNetworkApproval('unexpected' as never, request)).toEqual({
allowed: false,
reason: 'denied-by-policy'
})
expect(evaluateNetworkApproval('approved', { ...request, extra: true } as never)).toEqual({
allowed: false,
reason: 'invalid-request'
})
expect(evaluateNetworkApproval('approved', request, [{ ...callGrantForTest(), port: 0 }])).toEqual({
allowed: false,
reason: 'approval-required'
})
})
})

function callGrantForTest() {
return {
scope: 'host-port' as const,
host: 'api.example.com',
port: 443,
operation: 'read' as const
}
}
96 changes: 96 additions & 0 deletions kun/src/contracts/network-approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export const NETWORK_ACCESS_MODES = ['none', 'approved', 'full'] as const
export type NetworkAccessMode = typeof NETWORK_ACCESS_MODES[number]

export type NetworkOperation = 'read' | 'mutation'
export type NetworkApprovalScope = 'call' | 'host-port'

export type NetworkApprovalRequest = {
requestId: string
host: string
port: number
operation: NetworkOperation
}

export type NetworkApprovalGrant = {
scope: NetworkApprovalScope
requestId?: string
host: string
port: number
operation: NetworkOperation
allowLocalhost?: boolean
}

export type NetworkApprovalDecision = {
allowed: boolean
reason: 'full-access' | 'already-approved' | 'approval-required' | 'denied-by-policy' | 'invalid-request'
}

/** Pure network permission matching. It performs no DNS lookup or socket I/O. */
export function evaluateNetworkApproval(
mode: NetworkAccessMode,
request: NetworkApprovalRequest,
grants: readonly NetworkApprovalGrant[] = []
): NetworkApprovalDecision {
if (!NETWORK_ACCESS_MODES.includes(mode)) return { allowed: false, reason: 'denied-by-policy' }
const normalized = normalizeRequest(request)
if (!normalized) return { allowed: false, reason: 'invalid-request' }
if (mode === 'none') return { allowed: false, reason: 'denied-by-policy' }
if (mode === 'full') return { allowed: true, reason: 'full-access' }
if (grants.some((grant) => matchesGrant(grant, normalized))) {
return { allowed: true, reason: 'already-approved' }
}
return { allowed: false, reason: 'approval-required' }
}

export function normalizeNetworkHost(host: string): string | null {
if (typeof host !== 'string') return null
const normalized = host.trim().toLowerCase().replace(/\.$/, '')
if (!normalized || /[\s/@?#]/.test(normalized) || normalized.includes('..')) return null
if (normalized.startsWith('[') && !normalized.endsWith(']')) return null
return normalized
}

export function isLocalNetworkHost(host: string): boolean {
const normalized = normalizeNetworkHost(host)
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1' || normalized === '[::1]'
}

function normalizeRequest(request: NetworkApprovalRequest): NetworkApprovalRequest | null {
if (!request || typeof request !== 'object' ||
Object.keys(request).some((key) => !['requestId', 'host', 'port', 'operation'].includes(key))) return null
const host = normalizeNetworkHost(request?.host)
if (!host || !isRequestId(request?.requestId) || !isPort(request?.port)) return null
if (request.operation !== 'read' && request.operation !== 'mutation') return null
return { requestId: request.requestId, host, port: request.port, operation: request.operation }
}

function matchesGrant(grant: NetworkApprovalGrant, request: NetworkApprovalRequest): boolean {
if (!isValidGrant(grant)) return false
const host = normalizeNetworkHost(grant.host)
if (host !== request.host || grant.port !== request.port) return false
if (request.operation === 'mutation' && grant.operation !== 'mutation') return false
if (isLocalNetworkHost(request.host) && grant.allowLocalhost !== true) return false
return grant.scope === 'host-port' || grant.requestId === request.requestId
}

function isValidGrant(grant: NetworkApprovalGrant): boolean {
return Boolean(
grant &&
typeof grant === 'object' &&
Object.keys(grant).every((key) => ['scope', 'requestId', 'host', 'port', 'operation', 'allowLocalhost'].includes(key)) &&
(grant.scope === 'call' || grant.scope === 'host-port') &&
normalizeNetworkHost(grant.host) &&
isPort(grant.port) &&
(grant.operation === 'read' || grant.operation === 'mutation') &&
(grant.scope === 'host-port' || isRequestId(grant.requestId)) &&
(grant.allowLocalhost === undefined || typeof grant.allowLocalhost === 'boolean')
)
}

function isRequestId(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0 && value.length <= 128
}

function isPort(value: unknown): value is number {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= 65_535
}
Loading