import * as Address from './Address.js';
import * as Errors from './Errors.js';
import { uid } from './internal/uid.js';
export const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(:[0-9]{1,5})?$/;
export const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:[0-9]{1,5})?$/;
export const localhostRegex = /^localhost(:[0-9]{1,5})?$/;
export const nonceRegex = /^[a-zA-Z0-9]{8,}$/;
export const schemeRegex = /^([a-zA-Z][a-zA-Z0-9+-.]*)$/;
// https://regexr.com/80gdj
export const prefixRegex = /^(?:(?<scheme>[a-zA-Z][a-zA-Z0-9+-.]*):\/\/)?(?<domain>[a-zA-Z0-9+-.]*(?::[0-9]{1,5})?) (?:wants you to sign in with your Ethereum account:\n)(?<address>0x[a-fA-F0-9]{40})\n\n(?:(?<statement>.*)\n\n)?/;
// https://regexr.com/80gf9
export const suffixRegex = /(?:URI: (?<uri>.+))\n(?:Version: (?<version>.+))\n(?:Chain ID: (?<chainId>\d+))\n(?:Nonce: (?<nonce>[a-zA-Z0-9]+))\n(?:Issued At: (?<issuedAt>.+))(?:\nExpiration Time: (?<expirationTime>.+))?(?:\nNot Before: (?<notBefore>.+))?(?:\nRequest ID: (?<requestId>.+))?/;
/**
 * Creates [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) formatted message.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.createMessage({
 *   address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
 *   chainId: 1,
 *   domain: 'example.com',
 *   nonce: 'foobarbaz',
 *   uri: 'https://example.com/path',
 *   version: '1',
 * })
 * // @log: "example.com wants you to sign in with your Ethereum account:
 * // @log: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
 * // @log:
 * // @log:
 * // @log: URI: https://example.com/path
 * // @log: Version: 1
 * // @log: Chain ID: 1
 * // @log: Nonce: foobarbaz
 * // @log: Issued At: 2023-02-01T00:00:00.000Z"
 * ```
 *
 * @param value - Values to use when creating EIP-4361 formatted message.
 * @returns EIP-4361 formatted message.
 */
export function createMessage(value) {
    const { chainId, domain, expirationTime, issuedAt = new Date(), nonce, notBefore, requestId, resources, scheme, uri, version, } = value;
    // Validate fields
    {
        // Required fields
        if (chainId !== Math.floor(chainId))
            throw new InvalidMessageFieldError({
                field: 'chainId',
                metaMessages: [
                    '- Chain ID must be a EIP-155 chain ID.',
                    '- See https://eips.ethereum.org/EIPS/eip-155',
                    '',
                    `Provided value: ${chainId}`,
                ],
            });
        if (!(domainRegex.test(domain) ||
            ipRegex.test(domain) ||
            localhostRegex.test(domain)))
            throw new InvalidMessageFieldError({
                field: 'domain',
                metaMessages: [
                    '- Domain must be an RFC 3986 authority.',
                    '- See https://www.rfc-editor.org/rfc/rfc3986',
                    '',
                    `Provided value: ${domain}`,
                ],
            });
        if (!nonceRegex.test(nonce))
            throw new InvalidMessageFieldError({
                field: 'nonce',
                metaMessages: [
                    '- Nonce must be at least 8 characters.',
                    '- Nonce must be alphanumeric.',
                    '',
                    `Provided value: ${nonce}`,
                ],
            });
        if (!isUri(uri))
            throw new InvalidMessageFieldError({
                field: 'uri',
                metaMessages: [
                    '- URI must be a RFC 3986 URI referring to the resource that is the subject of the signing.',
                    '- See https://www.rfc-editor.org/rfc/rfc3986',
                    '',
                    `Provided value: ${uri}`,
                ],
            });
        if (version !== '1')
            throw new InvalidMessageFieldError({
                field: 'version',
                metaMessages: [
                    "- Version must be '1'.",
                    '',
                    `Provided value: ${version}`,
                ],
            });
        // Optional fields
        if (scheme && !schemeRegex.test(scheme))
            throw new InvalidMessageFieldError({
                field: 'scheme',
                metaMessages: [
                    '- Scheme must be an RFC 3986 URI scheme.',
                    '- See https://www.rfc-editor.org/rfc/rfc3986#section-3.1',
                    '',
                    `Provided value: ${scheme}`,
                ],
            });
        const statement = value.statement;
        if (statement?.includes('\n'))
            throw new InvalidMessageFieldError({
                field: 'statement',
                metaMessages: [
                    "- Statement must not include '\\n'.",
                    '',
                    `Provided value: ${statement}`,
                ],
            });
    }
    // Construct message
    const address = Address.from(value.address, { checksum: true });
    const origin = (() => {
        if (scheme)
            return `${scheme}://${domain}`;
        return domain;
    })();
    const statement = (() => {
        if (!value.statement)
            return '';
        return `${value.statement}\n`;
    })();
    const prefix = `${origin} wants you to sign in with your Ethereum account:\n${address}\n\n${statement}`;
    let suffix = `URI: ${uri}\nVersion: ${version}\nChain ID: ${chainId}\nNonce: ${nonce}\nIssued At: ${issuedAt.toISOString()}`;
    if (expirationTime)
        suffix += `\nExpiration Time: ${expirationTime.toISOString()}`;
    if (notBefore)
        suffix += `\nNot Before: ${notBefore.toISOString()}`;
    if (requestId)
        suffix += `\nRequest ID: ${requestId}`;
    if (resources) {
        let content = '\nResources:';
        for (const resource of resources) {
            if (!isUri(resource))
                throw new InvalidMessageFieldError({
                    field: 'resources',
                    metaMessages: [
                        '- Every resource must be a RFC 3986 URI.',
                        '- See https://www.rfc-editor.org/rfc/rfc3986',
                        '',
                        `Provided value: ${resource}`,
                    ],
                });
            content += `\n- ${resource}`;
        }
        suffix += content;
    }
    return `${prefix}\n${suffix}`;
}
/**
 * Generates random [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) nonce.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.generateNonce()
 * // @log: '65ed4681d4efe0270b923ff5f4b097b1c95974dc33aeebecd5724c42fd86dfd25dc70b27ef836b2aa22e68f19ebcccc1'
 * ```
 *
 * @returns Random nonce.
 */
export function generateNonce() {
    return uid(96);
}
/**
 * Check if the given URI is a valid [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) URI.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.isUri('https://example.com/foo')
 * // @log: true
 * ```
 *
 * @param value - Value to check.
 * @returns `false` if invalid, otherwise the valid URI.
 */
// based on https://github.com/ogt/valid-url
export function isUri(value) {
    // check for illegal characters
    if (/[^a-z0-9:/?#[\]@!$&'()*+,;=.\-_~%]/i.test(value))
        return false;
    // check for hex escapes that aren't complete
    if (/%[^0-9a-f]/i.test(value))
        return false;
    if (/%[0-9a-f](:?[^0-9a-f]|$)/i.test(value))
        return false;
    // from RFC 3986
    const splitted = splitUri(value);
    const scheme = splitted[1];
    const authority = splitted[2];
    const path = splitted[3];
    const query = splitted[4];
    const fragment = splitted[5];
    // scheme and path are required, though the path can be empty
    if (!(scheme?.length && path && path.length >= 0))
        return false;
    // if authority is present, the path must be empty or begin with a /
    if (authority?.length) {
        if (!(path.length === 0 || /^\//.test(path)))
            return false;
    }
    else {
        // if authority is not present, the path must not start with //
        if (/^\/\//.test(path))
            return false;
    }
    // scheme must begin with a letter, then consist of letters, digits, +, ., or -
    if (!/^[a-z][a-z0-9+\-.]*$/.test(scheme.toLowerCase()))
        return false;
    let out = '';
    // re-assemble the URL per section 5.3 in RFC 3986
    out += `${scheme}:`;
    if (authority?.length)
        out += `//${authority}`;
    out += path;
    if (query?.length)
        out += `?${query}`;
    if (fragment?.length)
        out += `#${fragment}`;
    return out;
}
function splitUri(value) {
    return value.match(/(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/);
}
/**
 * [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) formatted message into message fields object.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.parseMessage(`example.com wants you to sign in with your Ethereum account:
 * 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
 *
 * I accept the ExampleOrg Terms of Service: https://example.com/tos
 *
 * URI: https://example.com/path
 * Version: 1
 * Chain ID: 1
 * Nonce: foobarbaz
 * Issued At: 2023-02-01T00:00:00.000Z`)
 * // @log: {
 * // @log:   address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
 * // @log:   chainId: 1,
 * // @log:   domain: 'example.com',
 * // @log:   issuedAt: '2023-02-01T00:00:00.000Z',
 * // @log:   nonce: 'foobarbaz',
 * // @log:   statement: 'I accept the ExampleOrg Terms of Service: https://example.com/tos',
 * // @log:   uri: 'https://example.com/path',
 * // @log:   version: '1',
 * // @log: }
 * ```
 *
 * @param message - [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) formatted message.
 * @returns Message fields object.
 */
export function parseMessage(message) {
    const { scheme, statement, ...prefix } = (message.match(prefixRegex)
        ?.groups ?? {});
    const { chainId, expirationTime, issuedAt, notBefore, requestId, ...suffix } = (message.match(suffixRegex)?.groups ?? {});
    const resources = message.split('Resources:')[1]?.split('\n- ').slice(1);
    return {
        ...prefix,
        ...suffix,
        ...(chainId ? { chainId: Number(chainId) } : {}),
        ...(expirationTime ? { expirationTime: new Date(expirationTime) } : {}),
        ...(issuedAt ? { issuedAt: new Date(issuedAt) } : {}),
        ...(notBefore ? { notBefore: new Date(notBefore) } : {}),
        ...(requestId ? { requestId } : {}),
        ...(resources ? { resources } : {}),
        ...(scheme ? { scheme } : {}),
        ...(statement ? { statement } : {}),
    };
}
/**
 * Validates [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) message.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.validateMessage({
 *   address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
 *   domain: 'example.com',
 *   message: {
 *     address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
 *     chainId: 1,
 *     domain: 'example.com',
 *     nonce: 'foobarbaz',
 *     uri: 'https://example.com/path',
 *     version: '1',
 *   },
 *   nonce: 'foobarbaz',
 * })
 * // @log: true
 * ```
 *
 * @param value - Values to use when validating EIP-4361 formatted message.
 * @returns Whether the message is valid.
 */
export function validateMessage(value) {
    const { address, domain, message, nonce, scheme, time = new Date() } = value;
    if (domain && message.domain !== domain)
        return false;
    if (nonce && message.nonce !== nonce)
        return false;
    if (scheme && message.scheme !== scheme)
        return false;
    if (message.expirationTime && time >= message.expirationTime)
        return false;
    if (message.notBefore && time < message.notBefore)
        return false;
    try {
        if (!message.address)
            return false;
        if (address && !Address.isEqual(message.address, address))
            return false;
    }
    catch {
        return false;
    }
    return true;
}
/**
 * Thrown when a field in a SIWE Message is invalid.
 *
 * @example
 * ```ts twoslash
 * import { Siwe } from 'ox'
 *
 * Siwe.createMessage({
 *   address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
 *   chainId: 1.1,
 *   domain: 'example.com',
 *   nonce: 'foobarbaz',
 *   uri: 'https://example.com/path',
 *   version: '1',
 * })
 * // @error: Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "chainId".
 * // @error: - Chain ID must be a EIP-155 chain ID.
 * // @error: - See https://eips.ethereum.org/EIPS/eip-155
 * // @error: Provided value: 1.1
 * ```
 */
export class InvalidMessageFieldError extends Errors.BaseError {
    constructor(parameters) {
        const { field, metaMessages } = parameters;
        super(`Invalid Sign-In with Ethereum message field "${field}".`, {
            metaMessages,
        });
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'Siwe.InvalidMessageFieldError'
        });
    }
}
//# sourceMappingURL=Siwe.js.map