export type InputRecord = Record<string, unknown>
export type DiscordLane = 'bot' | 'oauth'
export const DEFAULT_BOT_SECRET_NAME = 'discordBotToken'
export const DEFAULT_OAUTH_INTEGRATION = 'discord'
export function inputRecord(value: unknown): InputRecord {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Input must be an object.')
}
return value as InputRecord
}
export function pickString(...values: unknown[]): string | null {
for (const value of values) {
if (typeof value === 'string' && value.trim().length > 0) return value.trim()
}
return null
}
export function requiredString(input: InputRecord, key: string): string {
const value = input[key]
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(key + ' is required.')
}
return value.trim()
}
export function optionalString(input: InputRecord, key: string): string | undefined {
const value = input[key]
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') throw new Error(key + ' must be a string.')
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : undefined
}
export function optionalBoolean(input: InputRecord, key: string): boolean | undefined {
const value = input[key]
if (value === undefined || value === null) return undefined
if (typeof value !== 'boolean') throw new Error(key + ' must be a boolean.')
return value
}
export function snowflake(input: InputRecord, key: string, aliases: string[] = []): string {
const value = pickString(input[key], ...aliases.map((alias) => input[alias]))
if (!value) throw new Error(key + ' is required.')
if (!/^[0-9]{5,32}$/.test(value)) {
throw new Error(key + ' must be a Discord snowflake id.')
}
return value
}
export function optionalSnowflake(input: InputRecord, key: string, aliases: string[] = []): string | undefined {
const value = pickString(input[key], ...aliases.map((alias) => input[alias]))
if (!value) return undefined
if (!/^[0-9]{5,32}$/.test(value)) {
throw new Error(key + ' must be a Discord snowflake id.')
}
return value
}
export function boundedInteger(value: unknown, fallback: number, min: number, max: number, key = 'limit'): number {
if (value === undefined || value === null || value === '') return fallback
if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
throw new Error(key + ' must be an integer from ' + min + ' through ' + max + '.')
}
return value
}
export function secretName(input: InputRecord): string {
const value = optionalString(input, 'secretName') ?? DEFAULT_BOT_SECRET_NAME
if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(value)) {
throw new Error('secretName must be a Kody secret name such as discordBotToken.')
}
return value
}
export function integrationName(input: InputRecord): string {
const value = optionalString(input, 'integration') ?? DEFAULT_OAUTH_INTEGRATION
if (!/^[a-z][a-z0-9._-]{0,63}$/.test(value)) {
throw new Error('integration must be a Kody integration name such as discord or discord-work.')
}
return value
}
export function requiredLane(input: InputRecord, fallback: DiscordLane = 'bot'): DiscordLane {
const value = input.lane
if (value === undefined || value === null || value === '') return fallback
if (value === 'bot' || value === 'oauth') return value
throw new Error("lane must be 'bot' or 'oauth'.")
}
export function clipText(value: string, maxLength: number): string {
const source = value.replace(/\s+/g, ' ').trim()
if (!source) return ''
return source.length <= maxLength ? source : source.slice(0, maxLength - 3).trimEnd() + '...'
}
export function requireLiveMutation(input: InputRecord, action: string): 'dryRun' | 'live' {
if (input.dryRun === true) return 'dryRun'
if (input.confirm !== true) {
throw new Error(
action +
' requires confirm: true after explicit approval of the destination and content. Pass dryRun: true to preview without calling Discord.',
)
}
return 'live'
}