import * as Errors from '../core/Errors.js' import * as Hash from '../core/Hash.js' import * as Hex from '../core/Hex.js' import type { Compute, PartialBy } from '../core/internal/types.js' import * as SignatureEnvelope from './SignatureEnvelope.js' import * as TempoAddress from './TempoAddress.js' /** * Header name used to transport Zone RPC authentication tokens. */ export const headerName = 'X-Authorization-Token' as const /** * 32-byte domain separator used when hashing Zone RPC authentication tokens. */ export const magicBytes = '0x54656d706f5a6f6e655250430000000000000000000000000000000000000000' as const /** * Size, in bytes, of the fixed Zone RPC authentication fields. */ export const fieldsSize = 29 as const /** Current Zone RPC authentication version. */ export const version = 0 as const /** Current Zone RPC authentication version. */ export type Version = typeof version /** * Root type for a Tempo Zone RPC authentication token. * * Zone RPC authentication tokens are short-lived, read-only credentials used to * authenticate requests to Tempo private zone RPC endpoints. * * [Zone RPC Specification](https://docs.tempo.xyz/protocol/privacy/rpc#authorization-tokens) */ export type ZoneRpcAuthentication< signed extends boolean = boolean, bigintType = bigint, numberType = number, > = Compute< { /** Zone chain ID for replay protection. */ chainId: numberType /** Unix timestamp when the token expires. */ expiresAt: numberType /** Unix timestamp when the token was issued. */ issuedAt: numberType /** Zone RPC authentication version. Always `0` for the current spec. */ version: Version /** Numeric zone identifier. */ zoneId: numberType } & (signed extends true ? { signature: SignatureEnvelope.SignatureEnvelope } : { signature?: | SignatureEnvelope.SignatureEnvelope | undefined }) > /** Input type for a Zone RPC authentication token. */ export type Input = PartialBy, 'version'> /** 29-byte fixed Zone RPC authentication field suffix. */ export type Fields = Hex.Hex /** Hex-encoded serialized Zone RPC authentication token. */ export type Serialized = Hex.Hex /** Signed Zone RPC authentication token. */ export type Signed< bigintType = bigint, numberType = number, > = ZoneRpcAuthentication /** * Instantiates a typed Zone RPC authentication token. * * @example * ```ts twoslash * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.from({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * ``` * * @example * ### Attaching Signatures * * ```ts twoslash * import { Secp256k1 } from 'ox' * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.from({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * * const signature = Secp256k1.sign({ * payload: ZoneRpcAuthentication.getSignPayload(authentication), * privateKey: '0x...', * }) * * const authentication_signed = ZoneRpcAuthentication.from(authentication, { * signature, * }) * ``` * * @param authentication - Zone RPC authentication token fields. * @param options - Zone RPC authentication options. * @returns The instantiated Zone RPC authentication token. */ export function from< const authentication extends Input | ZoneRpcAuthentication, const signature extends SignatureEnvelope.from.Value | undefined = undefined, >( authentication: authentication | ZoneRpcAuthentication, options: from.Options = {}, ): from.ReturnType { const auth = authentication as ZoneRpcAuthentication const resolved = { ...auth, version, } if (options.signature) return { ...resolved, signature: SignatureEnvelope.from(options.signature), } as never return resolved as never } export declare namespace from { type Options< signature extends SignatureEnvelope.from.Value | undefined = | SignatureEnvelope.from.Value | undefined, > = { /** The signature to attach to the authentication token. */ signature?: signature | SignatureEnvelope.SignatureEnvelope | undefined } type ReturnType< authentication extends | ZoneRpcAuthentication | Input = ZoneRpcAuthentication, signature extends SignatureEnvelope.from.Value | undefined = | SignatureEnvelope.from.Value | undefined, > = Compute< authentication & { readonly version: Version } & (signature extends SignatureEnvelope.from.Value ? { signature: SignatureEnvelope.from.ReturnValue } : {}) > type ErrorType = Errors.GlobalErrorType } /** * Parses a serialized Zone RPC authentication token. * * The serialized format is `<29-byte fields>`. The signature is parsed * from the prefix and the fixed-length fields are parsed from the suffix. * * @example * ```ts twoslash * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.deserialize('0x...') * ``` * * @param serialized - The serialized Zone RPC authentication token. * @returns The parsed Zone RPC authentication token. */ export function deserialize(serialized: Serialized): Signed { const size = Hex.size(serialized) if (size <= fieldsSize) throw new InvalidSerializedError({ reason: `Serialized authentication must be longer than ${fieldsSize} bytes.`, serialized, }) const fieldsOffset = size - fieldsSize const signature = Hex.slice(serialized, 0, fieldsOffset) const fields = Hex.slice(serialized, fieldsOffset) const parsedVersion = Hex.toNumber(Hex.slice(fields, 0, 1), { size: 1 }) if (parsedVersion !== version) throw new InvalidSerializedError({ reason: `Unsupported authentication version "${parsedVersion}". Expected "${version}".`, serialized, }) return { chainId: Hex.toNumber(Hex.slice(fields, 5, 13), { size: 8 }), expiresAt: Hex.toNumber(Hex.slice(fields, 21, 29), { size: 8 }), issuedAt: Hex.toNumber(Hex.slice(fields, 13, 21), { size: 8 }), signature: SignatureEnvelope.deserialize(signature), version, zoneId: Hex.toNumber(Hex.slice(fields, 1, 5), { size: 4 }), } } export declare namespace deserialize { type ErrorType = | InvalidSerializedError | SignatureEnvelope.CoercionError | SignatureEnvelope.InvalidSerializedError | Hex.size.ErrorType | Hex.slice.ErrorType | Hex.toNumber.ErrorType | Errors.GlobalErrorType } /** * Returns the 29-byte fixed field suffix for a Zone RPC authentication token. * * @example * ```ts twoslash * import { ZoneRpcAuthentication } from 'ox/tempo' * * const fields = ZoneRpcAuthentication.getFields({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * ``` * * @param authentication - The Zone RPC authentication token. * @returns The fixed 29-byte field suffix. */ export function getFields( authentication: PartialBy, ): Fields { return Hex.concat( Hex.fromNumber(version, { size: 1 }), Hex.fromNumber(authentication.zoneId, { size: 4 }), Hex.fromNumber(authentication.chainId, { size: 8 }), Hex.fromNumber(authentication.issuedAt, { size: 8 }), Hex.fromNumber(authentication.expiresAt, { size: 8 }), ) } export declare namespace getFields { type ErrorType = | Hex.concat.ErrorType | Hex.fromNumber.ErrorType | Errors.GlobalErrorType } /** * Computes the sign payload for a Zone RPC authentication token. * * When `userAddress` is provided, the payload is wrapped as * `keccak256(0x04 || authHash || userAddress)` to match V2 keychain signing. * * @example * ```ts twoslash * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.from({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * * const payload = ZoneRpcAuthentication.getSignPayload(authentication) * ``` * * @param authentication - The Zone RPC authentication token. * @param options - Options. * @returns The sign payload. */ export function getSignPayload( authentication: PartialBy, options: getSignPayload.Options = {}, ): Hex.Hex { const authHash = hash(authentication) if (options.userAddress) return Hash.keccak256( Hex.concat('0x04', authHash, TempoAddress.resolve(options.userAddress)), ) return authHash } export declare namespace getSignPayload { type Options = { /** * Root account address for keychain access-key signing. * * When provided, computes `keccak256(0x04 || authHash || userAddress)` * instead of the raw `authHash`. */ userAddress?: TempoAddress.Address | undefined } type ErrorType = hash.ErrorType | Errors.GlobalErrorType } /** * Computes the raw authorization hash for a Zone RPC authentication token. * * The hash is `keccak256(magicBytes || fields)`. * * @example * ```ts twoslash * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.from({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * * const hash = ZoneRpcAuthentication.hash(authentication) * ``` * * @param authentication - The Zone RPC authentication token. * @returns The authorization hash. */ export function hash( authentication: PartialBy, ): Hex.Hex { return Hash.keccak256(Hex.concat(magicBytes, getFields(authentication))) } export declare namespace hash { type ErrorType = | getFields.ErrorType | Hash.keccak256.ErrorType | Hex.concat.ErrorType | Errors.GlobalErrorType } /** * Serializes a Zone RPC authentication token to hex. * * The serialized format is `<29-byte fields>`. * * @example * ```ts twoslash * import { Secp256k1 } from 'ox' * import { ZoneRpcAuthentication } from 'ox/tempo' * * const authentication = ZoneRpcAuthentication.from({ * chainId: 4217000026, * expiresAt: 1711235160, * issuedAt: 1711234560, * zoneId: 26, * }) * * const signature = Secp256k1.sign({ * payload: ZoneRpcAuthentication.getSignPayload(authentication), * privateKey: '0x...', * }) * * const serialized = ZoneRpcAuthentication.serialize(authentication, { * signature, * }) * ``` * * @param authentication - The Zone RPC authentication token. * @param options - Serialization options. * @returns The serialized authentication token. */ export function serialize( authentication: PartialBy, options: serialize.Options = {}, ): Serialized { const signature = options.signature || authentication.signature if (!signature) throw new MissingSignatureError() return Hex.concat( SignatureEnvelope.serialize(SignatureEnvelope.from(signature)), getFields(authentication), ) } export declare namespace serialize { type Options = { /** Signature to attach to the serialized authentication token. */ signature?: SignatureEnvelope.from.Value | undefined } type ErrorType = | getFields.ErrorType | MissingSignatureError | SignatureEnvelope.CoercionError | Errors.GlobalErrorType } /** Error thrown when a serialized authentication token cannot be deserialized. */ export class InvalidSerializedError extends Errors.BaseError { override readonly name = 'ZoneRpcAuthentication.InvalidSerializedError' constructor({ reason, serialized }: { reason: string; serialized: Hex.Hex }) { super(`Unable to deserialize Zone RPC authentication: ${reason}`, { metaMessages: [`Serialized: ${serialized}`], }) } } /** Error thrown when serializing an authentication token without a signature. */ export class MissingSignatureError extends Errors.BaseError { override readonly name = 'ZoneRpcAuthentication.MissingSignatureError' constructor() { super('Zone RPC authentication is missing a signature.') } }