export type InputRecord = Record 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' }