Skip to main content

Try it Live

Run SIWE examples in the interactive playground

BrandedSiwe

Branded type for SIWE messages with type safety.

Overview

BrandedMessage provides type-safe SIWE message representation using TypeScript’s structural typing. All SIWE operations work with BrandedMessage type.

Type Definition

type BrandedMessage<
  TDomain extends string = string,
  TAddress extends AddressType = AddressType,
  TUri extends string = string,
  TVersion extends string = string,
  TChainId extends number = number,
> = {
  domain: TDomain;
  address: TAddress;
  statement?: string;
  uri: TUri;
  version: TVersion;
  chainId: TChainId;
  nonce: string;
  issuedAt: string;
  expirationTime?: string;
  notBefore?: string;
  requestId?: string;
  resources?: string[];
};

Generic Parameters

TDomain

  • Type: string literal type
  • Default: string
  • Purpose: Exact domain typing
  • Example: "example.com" vs string

TAddress

  • Type: AddressType
  • Default: AddressType
  • Purpose: Type-safe Ethereum address (20 bytes)

TUri

  • Type: string literal type
  • Default: string
  • Purpose: Exact URI typing
  • Example: "https://example.com" vs string

TVersion

  • Type: string literal type
  • Default: string
  • Purpose: Version typing (always “1” currently)

TChainId

  • Type: number literal type
  • Default: number
  • Purpose: Exact chain ID typing
  • Example: 1 vs number

Field Types

Required Fields

domain: TDomain;              // DNS authority
address: TAddress;            // 20-byte Ethereum address
uri: TUri;                    // RFC 3986 URI
version: TVersion;            // Current version "1"
chainId: TChainId;            // EIP-155 Chain ID
nonce: string;                // Min 8 chars, replay protection
issuedAt: string;             // ISO 8601 timestamp

Optional Fields

statement?: string;           // Human-readable assertion
expirationTime?: string;      // ISO 8601 expiration
notBefore?: string;           // ISO 8601 not-before
requestId?: string;           // System identifier
resources?: string[];         // Resource URIs

Type Examples

Basic Usage

import type { BrandedMessage } from 'tevm';

const message: BrandedMessage = {
  domain: "example.com",
  address: Address("0x..."),
  uri: "https://example.com",
  version: "1",
  chainId: 1,
  nonce: "abc123def",
  issuedAt: "2021-09-30T16:25:24.000Z",
};

Literal Type Preservation

const message = Siwe.create({
  domain: "example.com" as const,
  address: specificAddress,
  uri: "https://example.com" as const,
  chainId: 1 as const,
});

// message.domain type: "example.com" (literal)
// message.chainId type: 1 (literal)
// message.version type: "1" (always literal)

With Optional Fields

const message: BrandedMessage = {
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com",
  version: "1",
  chainId: 1,
  nonce: "abc123",
  issuedAt: "2021-09-30T16:25:24.000Z",
  statement: "Sign in to Example",
  expirationTime: "2021-10-01T16:25:24.000Z",
  resources: ["https://example.com/api"],
};

Type-Safe Functions

function authenticateUser(message: BrandedMessage): void {
  // TypeScript ensures all required fields present
  console.log(`Auth request from ${message.domain}`);
  console.log(`Address: ${Address.toHex(message.address)}`);
  console.log(`Chain: ${message.chainId}`);
}

// Type error if missing required fields
authenticateUser({ domain: "example.com" }); // Error

Namespace Pattern

BrandedSiwe follows namespace pattern for tree-shakeable methods:
export const BrandedSiwe = {
  create,
  format,
  generateNonce,
  getMessageHash,
  parse,
  validate,
  verify,
  verifyMessage,
};

Usage

import { BrandedSiwe } from 'tevm';

// Namespace access
const message = BrandedSiwe.create({ ... });
const text = BrandedSiwe.format(message);
const valid = BrandedSiwe.verify(message, signature);

// Individual imports (tree-shakeable)
import { create, format, verify } from 'tevm/BrandedSiwe';

const message = create({ ... });
const text = format(message);
const valid = verify(message, signature);

Type Guards

Runtime Type Checking

function isBrandedMessage(value: unknown): value is BrandedMessage {
  return (
    typeof value === 'object' &&
    value !== null &&
    'domain' in value &&
    'address' in value &&
    'uri' in value &&
    'version' in value &&
    'chainId' in value &&
    'nonce' in value &&
    'issuedAt' in value
  );
}

// Usage
if (isBrandedMessage(unknownValue)) {
  // TypeScript knows it's BrandedMessage
  const text = Siwe.format(unknownValue);
}

Validation Type Guard

function isValidMessage(message: BrandedMessage): message is BrandedMessage {
  const result = Siwe.validate(message);
  return result.valid;
}

// Usage
if (isValidMessage(message)) {
  // Message structure validated
  Siwe.verify(message, signature);
}

Signature

type Signature = Uint8Array;
65-byte ECDSA signature (r + s + v)

ValidationResult

type ValidationResult =
  | { valid: true }
  | { valid: false; error: ValidationError };
Discriminated union for validation results

ValidationError

type ValidationError =
  | { type: "invalid_domain"; message: string }
  | { type: "invalid_address"; message: string }
  | { type: "invalid_uri"; message: string }
  | { type: "invalid_version"; message: string }
  | { type: "invalid_chain_id"; message: string }
  | { type: "invalid_nonce"; message: string }
  | { type: "invalid_timestamp"; message: string }
  | { type: "expired"; message: string }
  | { type: "not_yet_valid"; message: string }
  | { type: "signature_mismatch"; message: string };
Discriminated union for error types

Type Safety Benefits

Compile-Time Checks

const message: BrandedMessage = {
  domain: "example.com",
  address: userAddress,
  // Error: Missing required fields
};

const complete: BrandedMessage = {
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com",
  version: "1",
  chainId: 1,
  nonce: "abc123",
  issuedAt: "2021-09-30T16:25:24.000Z",
}; // OK

Inference

// Type inferred from create
const message = Siwe.create({
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com",
  chainId: 1,
});
// message type: BrandedMessage<string, AddressType, string, "1", number>

// Function parameters infer type
function processMessage(msg: BrandedMessage) {
  // msg.domain: string
  // msg.chainId: number
  // msg.version: string
}

Optional Field Handling

function hasExpiration(message: BrandedMessage): boolean {
  // Type narrowing with optional chaining
  return message.expirationTime !== undefined;
}

function getExpiration(message: BrandedMessage): Date | undefined {
  // Safe optional field access
  return message.expirationTime
    ? new Date(message.expirationTime)
    : undefined;
}

Pattern Details

Data-Based Architecture

All Siwe code follows data-based pattern:
  • Data: TypeScript interfaces (BrandedMessage)
  • Methods: Namespace functions operating on data
  • No classes: Functions take data as first argument
// Data
const message: BrandedMessage = { ... };

// Methods operate on data
const text = BrandedSiwe.format(message);
const hash = BrandedSiwe.getMessageHash(message);
const valid = BrandedSiwe.verify(message, signature);

Tree-Shakeable

Individual functions can be imported:
import { create, format } from 'tevm/BrandedSiwe';
// Only create and format code included in bundle

Type Conventions

  • BrandedMessage - Message type
  • Signature - Signature type
  • ValidationResult - Result type
  • ValidationError - Error type
All follow Branded* or *Result naming.

Implementation Notes

  • Plain objects: No class instantiation overhead
  • Immutable operations: Functions don’t mutate inputs
  • Type-safe: Full TypeScript type checking
  • Serializable: JSON-compatible data structures
  • No brand field: Uses structural typing, no runtime brand

See Also