PHP WebShell

Текущая директория: /usr/lib/node_modules/bitgo/node_modules/ox/core

Просмотр файла: Siwe.ts

import * as Address from './Address.js'
import * as Errors from './Errors.js'
import type { ExactPartial } from './internal/types.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>.+))?/

/** [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) message fields. */
export type Message = {
  /**
   * The Ethereum address performing the signing.
   */
  address: Address.Address
  /**
   * The [EIP-155](https://eips.ethereum.org/EIPS/eip-155) Chain ID to which the session is bound,
   */
  chainId: number
  /**
   * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) authority that is requesting the signing.
   */
  domain: string
  /**
   * Time when the signed authentication message is no longer valid.
   */
  expirationTime?: Date | undefined
  /**
   * Time when the message was generated, typically the current time.
   */
  issuedAt?: Date | undefined
  /**
   * A random string typically chosen by the relying party and used to prevent replay attacks.
   */
  nonce: string
  /**
   * Time when the signed authentication message will become valid.
   */
  notBefore?: Date | undefined
  /**
   * A system-specific identifier that may be used to uniquely refer to the sign-in request.
   */
  requestId?: string | undefined
  /**
   * A list of information or references to information the user wishes to have resolved as part of authentication by the relying party.
   */
  resources?: string[] | undefined
  /**
   * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) URI scheme of the origin of the request.
   */
  scheme?: string | undefined
  /**
   * A human-readable ASCII assertion that the user will sign.
   */
  statement?: string | undefined
  /**
   * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) URI referring to the resource that is the subject of the signing (as in the subject of a claim).
   */
  uri: string
  /**
   * The current version of the SIWE Message.
   */
  version: '1'
}

/**
 * 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: Message): string {
  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}`
}

export declare namespace createMessage {
  type ErrorType =
    | Address.from.ErrorType
    | InvalidMessageFieldError
    | Errors.GlobalErrorType
}

/**
 * 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(): string {
  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: string): false | string {
  // 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: string) {
  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: string): ExactPartial<Message> {
  const { scheme, statement, ...prefix } = (message.match(prefixRegex)
    ?.groups ?? {}) as {
    address: Address.Address
    domain: string
    scheme?: string
    statement?: string
  }
  const { chainId, expirationTime, issuedAt, notBefore, requestId, ...suffix } =
    (message.match(suffixRegex)?.groups ?? {}) as {
      chainId: string
      expirationTime?: string
      issuedAt?: string
      nonce: string
      notBefore?: string
      requestId?: string
      uri: string
      version: '1'
    }
  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: validateMessage.Value): boolean {
  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
}

export declare namespace validateMessage {
  interface Value {
    /**
     * Ethereum address to check against.
     */
    address?: Address.Address | undefined
    /**
     * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) authority to check against.
     */
    domain?: string | undefined
    /**
     * EIP-4361 message fields.
     */
    message: ExactPartial<Message>
    /**
     * Random string to check against.
     */
    nonce?: string | undefined
    /**
     * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) URI scheme to check against.
     */
    scheme?: string | undefined
    /**
     * Current time to check optional `expirationTime` and `notBefore` fields.
     *
     * @default new Date()
     */
    time?: Date | undefined
  }
}

/**
 * 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 {
  override readonly name = 'Siwe.InvalidMessageFieldError'

  constructor(parameters: {
    field: string
    metaMessages?: string[] | undefined
  }) {
    const { field, metaMessages } = parameters
    super(`Invalid Sign-In with Ethereum message field "${field}".`, {
      metaMessages,
    })
  }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!