From c50b9945e4287f2206130b34025d25d16f213561 Mon Sep 17 00:00:00 2001 From: whitezaddy Date: Wed, 1 Jul 2026 02:35:12 +0100 Subject: [PATCH] refactor: add structured validation metadata and fix contract client test workspace instability --- src/config/sdkConfig.ts | 216 +++++++++---------------------- src/contracts/contractClient.ts | 25 ++++ src/contracts/contractHelpers.ts | 1 + src/errors/error.types.ts | 0 src/utils/errorTypes.ts | 0 src/utils/validation.ts | 179 +++++++++++++++++++------ 6 files changed, 227 insertions(+), 194 deletions(-) create mode 100644 src/errors/error.types.ts create mode 100644 src/utils/errorTypes.ts diff --git a/src/config/sdkConfig.ts b/src/config/sdkConfig.ts index 4d13938..959ad2d 100644 --- a/src/config/sdkConfig.ts +++ b/src/config/sdkConfig.ts @@ -6,7 +6,6 @@ import { CacheAdapter } from '../cache/cache.types'; import { ChainConfig } from '../contracts/contract.types'; import { validateAddress } from '../utils/validation'; -// GuildPass SDK: Exported component definition. export type GuildPassClientConfig = { apiUrl: string; chainId?: number; @@ -19,66 +18,31 @@ export type GuildPassClientConfig = { /** Global retry policy applied to all requests. Defaults to no retries. */ retry?: RetryConfig; hooks?: HttpHooks; - /** - * Optional fetch-compatible transport for tests, tracing, proxies, - * custom runtimes, or environments without globalThis.fetch. - */ fetch?: FetchLike; - /** - * When true, service responses are checked against runtime shape guards - * before being returned, throwing a GuildPassError with code - * INVALID_RESPONSE if the API response is malformed. Defaults to false - * to preserve existing behaviour. - */ validateResponses?: boolean; - /** - * A cache adapter used to memoize read responses. - * - * Provide `new InMemoryCacheAdapter()` for a built-in solution, or supply - * any object that satisfies the {@link CacheAdapter} interface (e.g. a - * Redis adapter) for distributed caching. - * - * @example - * ```typescript - * import { GuildPassClient, InMemoryCacheAdapter } from '@guildpass/sdk'; - * - * const client = new GuildPassClient({ - * apiUrl: 'https://api.guildpass.xyz', - * cache: new InMemoryCacheAdapter(), - * cacheTtl: 60_000, - * }); - * ``` - */ cache?: CacheAdapter; - /** - * Default TTL in **milliseconds** applied to every cached entry when - * a per-call TTL is not specified. Defaults to `0` (no expiry). - */ cacheTtl?: number; - /** - * Whether to send client metadata headers (`X-GuildPass-SDK-Version`, - * `X-GuildPass-Client`) on GuildPass API-relative requests. - * - * Defaults to `true`. Set to `false` to disable all metadata headers. - * Metadata headers never include API keys, wallet secrets, or tokens. - */ sendClientMetadata?: boolean; - /** - * Optional client or integration name (e.g. `"my-dapp"`, `"discord-bot"`). - * Appears in the `X-GuildPass-Client` header alongside the SDK version. - */ clientName?: string; - /** - * Optional client version string sent as part of `X-GuildPass-Client`. - * When omitted, only the client name is sent (if provided). - */ clientVersion?: string; - // GuildPass SDK: End of logic containment structure block. +}; + +/** + * Local helper to enforce structured configuration exceptions uniformly. + */ +const throwConfigError = (message: string, field: string, reason: string, value: any): never => { + const isSensitive = ['apikey', 'secret', 'privatekey', 'password'].includes(field.toLowerCase()); + throw new GuildPassError(message, GuildPassErrorCode.INVALID_CONFIG, undefined, { + field, + reason, + ...(isSensitive ? {} : { value }), + valueType: typeof value, + }); }; export function validateConfig(config: GuildPassClientConfig): void { if (!config.apiUrl) { - throw new GuildPassError('apiUrl is required', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('apiUrl is required', 'apiUrl', 'REQUIRED', config.apiUrl); } try { @@ -87,31 +51,15 @@ export function validateConfig(config: GuildPassClientConfig): void { throw new Error(); } } catch { - throw new GuildPassError( - `Invalid apiUrl: "${config.apiUrl}"`, - GuildPassErrorCode.INVALID_CONFIG, - ); + throwConfigError(`Invalid apiUrl: "${config.apiUrl}"`, 'apiUrl', 'INVALID_FORMAT', config.apiUrl); } - if ( - config.timeoutMs !== undefined && - (typeof config.timeoutMs !== 'number' || config.timeoutMs <= 0) - ) { - throw new GuildPassError( - 'timeoutMs must be a positive number', - GuildPassErrorCode.INVALID_CONFIG, - ); + if (config.timeoutMs !== undefined && (typeof config.timeoutMs !== 'number' || config.timeoutMs <= 0)) { + throwConfigError('timeoutMs must be a positive number', 'timeoutMs', 'INVALID_FORMAT', config.timeoutMs); } - // INSERT CACHE VALIDATION - if ( - config.cacheTtl !== undefined && - (typeof config.cacheTtl !== 'number' || config.cacheTtl < 0 || !Number.isFinite(config.cacheTtl)) - ) { - throw new GuildPassError( - 'cacheTtl must be a non-negative finite number (milliseconds)', - GuildPassErrorCode.INVALID_CONFIG, - ); + if (config.cacheTtl !== undefined && (typeof config.cacheTtl !== 'number' || config.cacheTtl < 0 || !Number.isFinite(config.cacheTtl))) { + throwConfigError('cacheTtl must be a non-negative finite number (milliseconds)', 'cacheTtl', 'INVALID_FORMAT', config.cacheTtl); } if (config.cache !== undefined) { @@ -119,140 +67,98 @@ export function validateConfig(config: GuildPassClientConfig): void { const required = ['get', 'set', 'delete', 'clear'] as const; for (const method of required) { if (typeof adapter[method] !== 'function') { - throw new GuildPassError( - `cache adapter must implement ${method}(): function`, - GuildPassErrorCode.INVALID_CONFIG, - ); + throwConfigError(`cache adapter must implement ${method}(): function`, 'cache', 'INVALID_TYPE', config.cache); } } } - // END CACHE VALIDATION - // INSERT METADATA VALIDATION - if ( - config.sendClientMetadata !== undefined && - typeof config.sendClientMetadata !== 'boolean' - ) { - throw new GuildPassError( - 'sendClientMetadata must be a boolean', - GuildPassErrorCode.INVALID_CONFIG, - ); + if (config.sendClientMetadata !== undefined && typeof config.sendClientMetadata !== 'boolean') { + throwConfigError('sendClientMetadata must be a boolean', 'sendClientMetadata', 'INVALID_TYPE', config.sendClientMetadata); } - if ( - config.clientName !== undefined && - typeof config.clientName !== 'string' - ) { - throw new GuildPassError( - 'clientName must be a string', - GuildPassErrorCode.INVALID_CONFIG, - ); + if (config.clientName !== undefined && typeof config.clientName !== 'string') { + throwConfigError('clientName must be a string', 'clientName', 'INVALID_TYPE', config.clientName); } - if ( - config.clientVersion !== undefined && - typeof config.clientVersion !== 'string' - ) { - throw new GuildPassError( - 'clientVersion must be a string', - GuildPassErrorCode.INVALID_CONFIG, - ); + if (config.clientVersion !== undefined && typeof config.clientVersion !== 'string') { + throwConfigError('clientVersion must be a string', 'clientVersion', 'INVALID_TYPE', config.clientVersion); } - // END METADATA VALIDATION - // INSERT RETRY VALIDATION if (config.retry) { const r = config.retry; if (r.maxRetries !== undefined && (typeof r.maxRetries !== 'number' || !Number.isFinite(r.maxRetries) || r.maxRetries < 0)) { - throw new GuildPassError('retry.maxRetries must be a non-negative finite number', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('retry.maxRetries must be a non-negative finite number', 'retry.maxRetries', 'INVALID_FORMAT', r.maxRetries); } if (r.baseDelayMs !== undefined && (typeof r.baseDelayMs !== 'number' || !Number.isFinite(r.baseDelayMs) || r.baseDelayMs < 0)) { - throw new GuildPassError('retry.baseDelayMs must be a non-negative finite number', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('retry.baseDelayMs must be a non-negative finite number', 'retry.baseDelayMs', 'INVALID_FORMAT', r.baseDelayMs); } if (r.maxDelayMs !== undefined && (typeof r.maxDelayMs !== 'number' || !Number.isFinite(r.maxDelayMs) || r.maxDelayMs < 0)) { - throw new GuildPassError('retry.maxDelayMs must be a non-negative finite number', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('retry.maxDelayMs must be a non-negative finite number', 'retry.maxDelayMs', 'INVALID_FORMAT', r.maxDelayMs); } if (r.baseDelayMs !== undefined && r.maxDelayMs !== undefined && r.maxDelayMs < r.baseDelayMs) { - throw new GuildPassError('retry.maxDelayMs cannot be less than baseDelayMs', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('retry.maxDelayMs cannot be less than baseDelayMs', 'retry.maxDelayMs', 'INVALID_FORMAT', r.maxDelayMs); } if (r.retryableStatuses !== undefined && (!Array.isArray(r.retryableStatuses) || r.retryableStatuses.length === 0 || r.retryableStatuses.some((s) => typeof s !== 'number' || !Number.isFinite(s)))) { - throw new GuildPassError('retryableStatuses must be a non-empty array of valid HTTP status numbers', GuildPassErrorCode.INVALID_CONFIG); + throwConfigError('retryableStatuses must be a non-empty array of valid HTTP status numbers', 'retry.retryableStatuses', 'INVALID_FORMAT', r.retryableStatuses); } } - // END RETRY VALIDATION + + if (config.apiKey !== undefined && typeof config.apiKey !== 'string') { + throwConfigError('apiKey must be a string', 'apiKey', 'INVALID_TYPE', config.apiKey); + } validateChainsConfig(config.chains); const transport = config.fetch ?? globalThis.fetch; if (typeof transport !== 'function') { - throw new GuildPassError( - 'A fetch-compatible transport is required. Provide config.fetch or use a runtime with globalThis.fetch.', - GuildPassErrorCode.INVALID_CONFIG, - ); + throwConfigError('A fetch-compatible transport is required.', 'fetch', 'REQUIRED', null); } } function validateChainsConfig(chains?: Record): void { - if (!chains) { - return; - } + if (!chains) return; for (const [chainIdKey, chainConfig] of Object.entries(chains)) { const chainId = Number(chainIdKey); if (!Number.isSafeInteger(chainId) || chainId <= 0 || String(chainId) !== chainIdKey) { - throw new GuildPassError( - `Invalid chains[${chainIdKey}]: chain ID must be a positive safe integer`, - GuildPassErrorCode.INVALID_CONFIG, - ); + throwConfigError(`Invalid chains[${chainIdKey}]: chain ID must be a positive safe integer`, `chains.${chainIdKey}`, 'INVALID_FORMAT', chainIdKey); } if (chainConfig.rpcUrl !== undefined) { - validateChainRpcUrl(chainIdKey, chainConfig.rpcUrl); + try { + const url = new URL(chainConfig.rpcUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error(); + } catch { + throwConfigError(`Invalid chains[${chainIdKey}].rpcUrl: expected an http or https URL`, `chains.${chainIdKey}.rpcUrl`, 'INVALID_FORMAT', chainConfig.rpcUrl); + } } if (chainConfig.contractAddress !== undefined) { - validateChainContractAddress(chainIdKey, chainConfig.contractAddress); - } - } -} - -function validateChainRpcUrl(chainIdKey: string, rpcUrl: string): void { - try { - const url = new URL(rpcUrl); - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - throw new Error(); + try { + validateAddress(chainConfig.contractAddress); + } catch (err: any) { + throw new GuildPassError( + `Invalid chains[${chainIdKey}].contractAddress: expected a valid EVM address`, + GuildPassErrorCode.INVALID_CONFIG, + undefined, + { + field: `chains.${chainIdKey}.contractAddress`, + reason: 'INVALID_FORMAT', + value: chainConfig.contractAddress, + valueType: 'string', + } + ); + } } - } catch { - throw new GuildPassError( - `Invalid chains[${chainIdKey}].rpcUrl: expected an http or https URL`, - GuildPassErrorCode.INVALID_CONFIG, - ); } } -function validateChainContractAddress(chainIdKey: string, contractAddress: string): void { - try { - validateAddress(contractAddress); - } catch { - throw new GuildPassError( - `Invalid chains[${chainIdKey}].contractAddress: expected a valid EVM address`, - GuildPassErrorCode.INVALID_CONFIG, - ); - } -} -/** - * Resolves the chain configuration for a given chain ID. - * Per-chain entries in `config.chains` take precedence over the top-level - * `rpcUrl` / `contractAddress` fallbacks. - * Throws `INVALID_CONFIG` only when a `chains` map is provided but does not - * contain an entry for the requested chain. - */ export function resolveChainConfig(config: GuildPassClientConfig, chainId: number): ChainConfig { if (config.chains) { if (Object.prototype.hasOwnProperty.call(config.chains, chainId)) { @@ -261,7 +167,9 @@ export function resolveChainConfig(config: GuildPassClientConfig, chainId: numbe throw new GuildPassError( `No configuration found for chain ID ${chainId}`, GuildPassErrorCode.INVALID_CONFIG, + undefined, + { field: 'chainId', reason: 'NOT_FOUND', value: chainId, valueType: 'number' } ); } return { rpcUrl: config.rpcUrl, contractAddress: config.contractAddress }; -} +} \ No newline at end of file diff --git a/src/contracts/contractClient.ts b/src/contracts/contractClient.ts index 7f027c2..35d508d 100644 --- a/src/contracts/contractClient.ts +++ b/src/contracts/contractClient.ts @@ -19,6 +19,7 @@ import { import { BALANCE_OF_SELECTOR, GET_GUILD_OWNER_SELECTOR, + DECIMALS_SELECTOR, // <-- ADD THIS IMPORT HEX_32_BYTES_LENGTH, decodeAddressResult, decodeUint256Result, @@ -30,9 +31,31 @@ import { GuildPassClientConfig, resolveChainConfig } from '../config/sdkConfig'; import { HttpClient } from '../http/httpClient'; import { RequestOptions } from '../types/common'; +// Local pure helper function for exact decimal string shift math +export const formatUnits = (value: string, decimals: number): string => { + // Guard against negative decimal counts or non-integers + if (decimals < 0 || !Number.isInteger(decimals)) { + throw new Error('Decimals must be a non-negative integer'); + } + + // Guard against invalid base unit numeric strings (letters or pre-existing decimals) + if (!/^\d+$/.test(value)) { + throw new Error('Value must be a valid big integer string containing only digits'); + } + + if (value === '0' || !value) return '0'; + + const padded = value.padStart(decimals + 1, '0'); + const loc = padded.length - decimals; + const whole = padded.slice(0, loc); + let fraction = padded.slice(loc).replace(/0+$/, ''); + return fraction ? `${whole}.${fraction}` : whole; +}; + export { BALANCE_OF_SELECTOR, GET_GUILD_OWNER_SELECTOR, + DECIMALS_SELECTOR, // <-- ADD THIS EXPORT HEX_32_BYTES_LENGTH, decodeAddressResult, decodeUint256Result, @@ -40,6 +63,8 @@ export { encodeGuildId, }; + + type JsonRpcSuccess = { result?: unknown; }; diff --git a/src/contracts/contractHelpers.ts b/src/contracts/contractHelpers.ts index 7c3baf0..c78ca71 100644 --- a/src/contracts/contractHelpers.ts +++ b/src/contracts/contractHelpers.ts @@ -15,6 +15,7 @@ export const BALANCE_OF_SELECTOR = '0x70a08231'; export const ERC721_OWNER_OF_SELECTOR = '0x6352211e'; /** OpenZeppelin AccessControl `hasRole(bytes32,address)`. */ export const HAS_ROLE_SELECTOR = '0x91d14854'; +export const DECIMALS_SELECTOR = '0x313ce567'; // 4-byte signature for decimals() export const HEX_32_BYTES_LENGTH = 64; diff --git a/src/errors/error.types.ts b/src/errors/error.types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/errorTypes.ts b/src/utils/errorTypes.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/validation.ts b/src/utils/validation.ts index e1e1e21..5de6a7e 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -5,6 +5,33 @@ import { GuildPassErrorCode } from '../errors/errorCodes'; // GuildPass SDK: Pull in package or module bindings. import { isChecksumAddress } from './address'; +/** + * Interface detailing structured error metadata for downstream consumers. + */ +export interface ValidationErrorDetails { + field: string; + reason: 'REQUIRED' | 'INVALID_FORMAT' | 'CHECKSUM_FAILED' | 'INVALID_TYPE'; + value?: any; + valueType: string; +} + +/** + * Common validation error helper to keep error shapes consistent across the SDK. + */ +const throwValidationError = ( + message: string, + code: GuildPassErrorCode, + details: ValidationErrorDetails, +): never => { + // Safe-guard: Mask any potential hidden fields or raw secrets from leaking down + const sensitiveKeys = ['apikey', 'secret', 'privatekey', 'password', 'token']; + if (sensitiveKeys.includes(details.field.toLowerCase())) { + delete details.value; + } + + throw new GuildPassError(message, code, undefined, details); +}; + /** * Validates an Ethereum address. * @@ -12,35 +39,33 @@ import { isChecksumAddress } from './address'; * @param options Validation options to enforce strict mode * @throws GuildPassError if the address is invalid */ -// GuildPass SDK: Exported component definition. export const validateAddress = (address: string, options: { strict?: boolean } = {}): void => { - // GuildPass SDK: Evaluate branch condition logic. if (!address) { - // GuildPass SDK: Raise exceptional condition and throw error. - throw new GuildPassError('Address is required', GuildPassErrorCode.INVALID_INPUT); - // GuildPass SDK: End of logic containment structure block. + throwValidationError('Address is required', GuildPassErrorCode.INVALID_INPUT, { + field: 'address', + reason: 'REQUIRED', + valueType: 'string', + }); } - // GuildPass SDK: Define internal reference identifier. const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/; - // GuildPass SDK: Conditional check guard path. if (!ethAddressRegex.test(address)) { - // GuildPass SDK: Propagate error state with specific code description. - throw new GuildPassError( - `Invalid Ethereum address: ${address}`, - GuildPassErrorCode.INVALID_ADDRESS, - ); - // GuildPass SDK: End of logic containment structure block. + throwValidationError(`Invalid Ethereum address: ${address}`, GuildPassErrorCode.INVALID_ADDRESS, { + field: 'address', + reason: 'INVALID_FORMAT', + value: address, + valueType: typeof address, + }); } - // GuildPass SDK: Evaluate branch condition logic. if (options.strict && !isChecksumAddress(address)) { - throw new GuildPassError( - `Address fails EIP-55 checksum: ${address}`, - GuildPassErrorCode.INVALID_ADDRESS, - ); + throwValidationError(`Address fails EIP-55 checksum: ${address}`, GuildPassErrorCode.INVALID_ADDRESS, { + field: 'address', + reason: 'CHECKSUM_FAILED', + value: address, + valueType: typeof address, + }); } - // GuildPass SDK: End of logic containment structure block. }; /** @@ -49,15 +74,31 @@ export const validateAddress = (address: string, options: { strict?: boolean } = * @param guildId The guild ID to validate * @throws GuildPassError if the guild ID is invalid */ -// GuildPass SDK: Exposed interface structure. export const validateGuildId = (guildId: string): void => { - // GuildPass SDK: Verify constraint requirements before proceeding. - if (!guildId || typeof guildId !== 'string' || guildId.trim().length === 0) { - // GuildPass SDK: Raise exceptional condition and throw error. - throw new GuildPassError('Invalid Guild ID', GuildPassErrorCode.INVALID_INPUT); - // GuildPass SDK: End of logic containment structure block. + if (guildId === undefined || guildId === null) { + throwValidationError('Guild ID is required', GuildPassErrorCode.INVALID_INPUT, { + field: 'guildId', + reason: 'REQUIRED', + valueType: 'undefined', + }); + } + + if (typeof guildId !== 'string') { + throwValidationError('Guild ID must be a string', GuildPassErrorCode.INVALID_INPUT, { + field: 'guildId', + reason: 'INVALID_TYPE', + valueType: typeof guildId, + }); + } + + if (guildId.trim().length === 0) { + throwValidationError('Invalid Guild ID: cannot be empty', GuildPassErrorCode.INVALID_INPUT, { + field: 'guildId', + reason: 'INVALID_FORMAT', + value: guildId, + valueType: 'string', + }); } - // GuildPass SDK: End of logic containment structure block. }; /** @@ -66,15 +107,31 @@ export const validateGuildId = (guildId: string): void => { * @param resourceId The resource ID to validate * @throws GuildPassError if the resource ID is invalid */ -// GuildPass SDK: Core operational type definition. export const validateResourceId = (resourceId: string): void => { - // GuildPass SDK: Evaluate branch condition logic. - if (!resourceId || typeof resourceId !== 'string' || resourceId.trim().length === 0) { - // GuildPass SDK: Propagate error state with specific code description. - throw new GuildPassError('Invalid Resource ID', GuildPassErrorCode.INVALID_INPUT); - // GuildPass SDK: End of logic containment structure block. + if (resourceId === undefined || resourceId === null) { + throwValidationError('Resource ID is required', GuildPassErrorCode.INVALID_INPUT, { + field: 'resourceId', + reason: 'REQUIRED', + valueType: 'undefined', + }); + } + + if (typeof resourceId !== 'string') { + throwValidationError('Resource ID must be a string', GuildPassErrorCode.INVALID_INPUT, { + field: 'resourceId', + reason: 'INVALID_TYPE', + valueType: typeof resourceId, + }); + } + + if (resourceId.trim().length === 0) { + throwValidationError('Invalid Resource ID: cannot be empty', GuildPassErrorCode.INVALID_INPUT, { + field: 'resourceId', + reason: 'INVALID_FORMAT', + value: resourceId, + valueType: 'string', + }); } - // GuildPass SDK: End of logic containment structure block. }; /** @@ -83,13 +140,55 @@ export const validateResourceId = (resourceId: string): void => { * @param roleId The role ID to validate * @throws GuildPassError if the role ID is invalid */ -// GuildPass SDK: Exported function execution unit. export const validateRoleId = (roleId: string): void => { - // GuildPass SDK: Conditional check guard path. - if (!roleId || typeof roleId !== 'string' || roleId.trim().length === 0) { - // GuildPass SDK: Raise exceptional condition and throw error. - throw new GuildPassError('Invalid Role ID', GuildPassErrorCode.INVALID_INPUT); - // GuildPass SDK: End of logic containment structure block. + if (roleId === undefined || roleId === null) { + throwValidationError('Role ID is required', GuildPassErrorCode.INVALID_INPUT, { + field: 'roleId', + reason: 'REQUIRED', + valueType: 'undefined', + }); + } + + if (typeof roleId !== 'string') { + throwValidationError('Role ID must be a string', GuildPassErrorCode.INVALID_INPUT, { + field: 'roleId', + reason: 'INVALID_TYPE', + valueType: typeof roleId, + }); + } + + if (roleId.trim().length === 0) { + throwValidationError('Invalid Role ID: cannot be empty', GuildPassErrorCode.INVALID_INPUT, { + field: 'roleId', + reason: 'INVALID_FORMAT', + value: roleId, + valueType: 'string', + }); + } +}; + +/** + * Validates generic configuration inputs, filtering out sensitive properties. + */ +export const validateConfigField = ( + field: string, + value: any, + rules: { required?: boolean; expectedType?: string }, +): void => { + if (rules.required && (value === undefined || value === null || (typeof value === 'string' && value.trim().length === 0))) { + throwValidationError(`Configuration field "${field}" is required`, GuildPassErrorCode.INVALID_INPUT, { + field, + reason: 'REQUIRED', + valueType: typeof value, + }); + } + + if (rules.expectedType && value !== undefined && value !== null && typeof value !== rules.expectedType) { + throwValidationError(`Configuration field "${field}" expected type ${rules.expectedType}`, GuildPassErrorCode.INVALID_INPUT, { + field, + reason: 'INVALID_TYPE', + value: value, + valueType: typeof value, + }); } - // GuildPass SDK: End of logic containment structure block. };