DocsReference

Types

Every type exported by @galabet/fair 0.1.0, as the source declares it, with the notes a definition cannot carry.

Sixteen types are exported from @galabet/fair. The definitions below are copied from packages/fair/src/, comments included, with line breaks added to the ones the source writes on a single line. Functions are on the API page. TypeScript checks shapes and nothing else here: every range on this page is enforced at run time or not at all, and the notes say which.

Everything in one import
import type {
  Hex, SeedPair, Commitment, GameName, GameParams, FairRecord,
  FloatStream, PlayInput, PlayOutput, VerifyOutcome, GameDefinition,
  KeyPair, CrashChain, Inspection, InspectionCheck, CheckState,
} from '@galabet/fair';

@galabet/fair/games exports one type, GameDefinition. Asking it for GameParams or GameName is error TS2305. Take those from the main entry.

Hex

types.ts
/** Hex string, lowercase, no 0x prefix. */
export type Hex = string;

An alias, so the compiler accepts any string. Lowercase and length are checked by the functions that receive one.

SeedPair

types.ts
/** The three inputs that fully determine a bet under the single-player profile. */
export interface SeedPair {
  /** 32 random bytes, hex encoded (64 chars). Revealed only on rotation. */
  serverSeed: Hex;
  /** Player-chosen UTF-8 string, 1 to 64 chars. */
  clientSeed: string;
  /** Bet counter, starts at 0 for every new seed pair. */
  nonce: number;
}

The comment on clientSeed is looser than the code. The limit is 64 UTF-16 code units, which is what length counts, and a colon is refused. nonce has to be a whole number of 0 or more. A numeric string is a compile error in TypeScript. From JavaScript it fails at run time with nonce must be a non-negative integer.

Commitment

types.ts
/** A published commitment: the SHA-256 of the server seed, known before any bet. */
export interface Commitment {
  commitment: Hex;
  /** Unix ms when the commitment was published. Operators should persist this. */
  publishedAt: number;
}

This is what commit resolves to. publishedAt is Date.now() at the moment of the call, so it records when the hash was computed. Publishing is something the operator does afterwards, and the time worth persisting is that one.

GameName and GameParams

types.ts
export type GameName =
  | 'dice'
  | 'limbo'
  | 'roulette'
  | 'wheel'
  | 'plinko'
  | 'mines'
  | 'keno'
  | 'blackjack'
  | 'hilo';

/** Game specific parameters carried in every record so the result can be reproduced. */
export interface GameParams {
  /** Declared house edge, 0 to 1. Never hidden inside a mapper. */
  houseEdge?: number;
  /** wheel: number of segments. */
  segments?: number;
  /** plinko: number of rows (8 to 16). */
  rows?: number;
  /** mines: number of mines on the 5x5 grid (1 to 24). */
  mines?: number;
  /** keno: numbers drawn, default 10. */
  draws?: number;
  /** blackjack / hilo: number of decks, default 1. */
  decks?: number;
}

Crash is absent from GameName because it isn't played through play. isGameName narrows a string to this union.

GameParams is one bag for all nine games, so the type lets you write { game: 'dice', params: { decks: 4 } }. Each game reads one key at most and ignores the others; inspectRecord rejects the extras. Only Limbo reads houseEdge, and play doesn't hold it to the 0 to 1 in the comment. The defaults and enforced ranges are in the table under play.

GameDefinition

games/index.ts
export interface GameDefinition {
  /** Floats the mapper consumes for the given params. */
  floats: (params: GameParams) => number;
  /** Pure mapper from floats to a result. */
  map: (floats: readonly number[], params: GameParams) => unknown;
}

The value type of GAMES. floats runs before map has validated anything, which is how a bad rows turns into count must be a positive integer.

FairRecord

types.ts
/** The canonical verification record. Field order is irrelevant: records are canonicalised before hashing. */
export interface FairRecord {
  spec: 'GFS/1.0';
  profile: 'single-player';
  game: GameName;
  params: GameParams;
  /** Present only after rotation. Before that, verify against `commitment`. */
  serverSeed?: Hex;
  commitment: Hex;
  clientSeed: string;
  nonce: number;
  /** Highest cursor consumed for this bet. 0 for games needing at most 8 floats. */
  cursor: number;
  result: unknown;
  /** Unix ms. */
  at: number;
  /** Optional GFS/1.1 beacon fields. */
  beacon?: { source: 'drand' | 'evm'; ref: string; value: Hex };
  /** Optional operator signature over the canonical record (Ed25519, hex). */
  signature?: Hex;
  signer?: Hex;
}

Four fields are optional, for different reasons. serverSeed is absent while the seed is live and added once, at reveal. signature and signer come as a pair from signRecord, 128 and 64 hex characters, and a record with only one of them fails signature verification. beacon is there so a record from a future GFS 1.1 can be carried without loss. Nothing in 0.1.0 reads it, and the union 'drand' | 'evm' describes a plan.

params is required even when it is {}. spec, profile and game are literal types. An object you build in a variable and pass along later has them widened to string, and verifyRecord(record) is then error TS2345. Annotate the variable as FairRecord, or put as const on those three values. JSON parsed at run time is any and gets no checking at all, which is the case inspectRecord exists for. The record format page describes each field's meaning.

Why Result Is Unknown

result is typed unknown here, on PlayOutput and on both sides of VerifyOutcome. The shape depends on game, and the record type isn't a discriminated union over the nine games, so the compiler has nothing to narrow on. unknown is the honest type for that: const n: number = out.result is error TS2322, and you have to say which game you mean.

GameResult typeWith the public inputs and default parameters
dicenumber, 0 to 100 in hundredths56.12
limbonumber, 1 or more, in hundredths1.76
roulettenumber, whole, 0 to 3620
wheelnumber, whole, 0 to segments - 15
plinko{ path: (0 | 1)[]; bucket: number }16 steps, bucket 0 to rows
minesnumber[], tiles 0 to 24, ascending[9, 17, 22]
kenonumber[], numbers 1 to 40, ascending10 numbers
blackjack, hilostring[], labels such as 7C, in dealing order52 labels

The mappers in @galabet/fair/games are typed precisely, so calling dice(f) directly gives you a number with no cast. The Plinko interface is the one gap. The source declares it, and neither entry point exports it:

games/plinko.ts
export interface PlinkoResult {
  path: (0 | 1)[];
  bucket: number;
}

Importing PlinkoResult from either path is TS2305. ReturnType<typeof plinko> recovers it, and a small map gives play a typed result. This compiles under strict with TypeScript 5.7:

typed-play.ts
import { play } from '@galabet/fair';
import type { GameName, PlayInput } from '@galabet/fair';
import type { plinko } from '@galabet/fair/games';

interface ResultOf {
  dice: number;
  limbo: number;
  roulette: number;
  wheel: number;
  plinko: ReturnType<typeof plinko>;
  mines: number[];
  keno: number[];
  blackjack: string[];
  hilo: string[];
}

async function playTyped<G extends GameName>(input: PlayInput & { game: G }): Promise<ResultOf[G]> {
  const { result } = await play(input);
  return result as ResultOf[G];
}

The cast is safe for a result that came from play. It is not safe for record.result on a record someone sent you, where the value is whatever they typed. Check that at run time:

result-shapes.mjs
import { GAMES, play } from '@galabet/fair';

const seeds = {
  serverSeed: '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d',
  clientSeed: 'galabet',
  nonce: 42,
};

function shape(value) {
  if (Array.isArray(value)) return `${typeof value[0]}[] of ${value.length}`;
  if (value && typeof value === 'object') return `{ ${Object.keys(value).join(', ')} }`;
  return typeof value;
}

for (const game of Object.keys(GAMES)) {
  const { result } = await play({ game, ...seeds });
  console.log(game.padEnd(9), shape(result));
}
Output
dice      number
limbo     number
roulette  number
wheel     number
plinko    { path, bucket }
mines     number[] of 3
keno      number[] of 10
blackjack string[] of 52
hilo      string[] of 52

PlayInput and PlayOutput

verify.ts
export interface PlayInput extends SeedPair {
  game: GameName;
  params?: GameParams;
}

export interface PlayOutput {
  result: unknown;
  cursor: number;
  floats: number[];
}

params is optional on the way in and treated as {} when missing. A record has no such allowance. cursor is the highest cursor index read, counting from 0, and floats holds exactly the floats the game consumed: one for Dice, 24 for Mines whatever the mine count.

FloatStream

derive.ts
export interface FloatStream {
  floats: number[];
  /** Highest cursor consumed. Goes into the record. */
  cursor: number;
}

Returned by deriveFloats. Each float is a multiple of 2⁻³² from 0 up to but not including 1.

VerifyOutcome

verify.ts
export interface VerifyOutcome {
  ok: boolean;
  /** What the seeds actually produce. */
  computed: unknown;
  /** What the record claims. */
  claimed: unknown;
  commitmentOk: boolean;
  cursorOk: boolean;
  /** null when the record is unsigned, else whether the signature verifies under `signer`. */
  signatureOk: boolean | null;
  recordHash: string;
  reasons: string[];
}

ok is reasons.length === 0 and nothing more. The three flags don't vote: commitmentOk and cursorOk are false on a record with no serverSeed, where nothing was checked, and computed is null there too. signatureOk is three-valued, so test it with === false and not with !. recordHash is the hash of the record as given, which means it differs before and after reveal. The strings that can appear in reasons are listed on the errors page.

CheckState, InspectionCheck and Inspection

inspect.ts
export type CheckState = 'matches' | 'mismatch' | 'not-provided' | 'unsupported';

export interface InspectionCheck {
  name: string;
  state: CheckState;
  detail: string;
}

export interface Inspection {
  kind: string;
  status: 'matches' | 'mismatch' | 'incomplete';
  computed: unknown;
  claimed: unknown;
  checks: InspectionCheck[];
  difference: string | null;
  recordHash?: string | undefined;
  note: string;
}

name and kind are typed string, and in 0.1.0 they take a small set of values. name is one of Commitment, Cursor, Signature, Outcome, Chain link or Beacon. kind is a GameName, Crash or Galabet Flight. Neither set is part of the type, so a switch over them gets no exhaustiveness check.

recordHash is the only optional field. It is set for a single-player record that has result, cursor, at and commitment, and never for a crash record. difference is a sentence naming the first place the claimed and computed results part ways, or null when they agree or when one of them is missing. computed and claimed are null, not undefined, when absent, which also means a record whose result is literally null reads as having no result. detail and note are prose for a person and shouldn't be parsed. How status is derived is covered under inspectRecord.

KeyPair

sign.ts
export interface KeyPair {
  /** 32 bytes hex. Publish this. */
  publicKey: Hex;
  /** 64 bytes hex: seed || publicKey. Keep secret. */
  secretKey: Hex;
}

64 and 128 characters. The second half of secretKey is publicKey again, so the secret is the first 64 characters.

CrashChain

crash.ts
export interface CrashChain {
  /** h_0. Keep secret until the chain is exhausted. */
  secret: Hex;
  /** Published before game 1. */
  terminatingHash: Hex;
  length: number;
}

length is the number of games, 1 to 10,000,000 when the chain comes from createCrashChain. The object is plain data and nothing validates one you build by hand: crashGameHash and expandCrashChain trust secret and length, and never read terminatingHash. Crash rounds have no FairRecord. The shape inspectRecord accepts for them (gameHash, salt, houseEdge and the optional fields) has no exported type in 0.1.0.