DocsReference

Errors

Every message @galabet/fair 0.1.0 can throw, which function throws it and why, plus the reason strings verifyRecord returns.

Version 0.1.0 throws plain Error objects. There is a message and nothing else: no code, no subclass, no cause. That is a gap. Matching on message text breaks the day a message is reworded, so until codes exist the safer pattern is to validate before calling, with assertServerSeed, assertClientSeed and isGameName, and to treat anything caught as "bad input" without parsing it. The messages below are copied from the source. Angle brackets mark a part that varies, such as <rows>.

A thrown error travels up through every caller. play calls deriveFloats, which calls deriveDigest, which runs both seed assertions, so a bad client seed surfaces from all four with the same text. The "thrown by" column names the function that owns the message and then the exported functions it can escape from.

Seeds

MessageThrown byCause
server seed must be 64 lowercase hex charactersassertServerSeed. Escapes from commit, verifyCommitment, deriveDigest, deriveFloats, play, verifyRecordWrong length, a character outside 0-9a-f, capital letters, or not a string
client seed must be 1 to 64 charactersassertClientSeed. Escapes from deriveDigest, deriveFloats, play, verifyRecord, inspectRecordEmpty, longer than 64 UTF-16 code units, or not a string
client seed must not contain ":" (reserved as the HMAC message separator)same as aboveA colon anywhere in the seed

Derivation

MessageThrown byCause
nonce must be a non-negative integerderiveDigest. Escapes from deriveFloats, play, verifyRecordNegative, fractional, NaN, or a string such as "42"
cursor must be a non-negative integerderiveDigestSame rule, for the second argument
count must be a positive integerderiveFloats. Escapes from play, verifyRecordZero, negative or fractional count. From play it means rows was below 1 or fractional, or decks was below 1. The message doesn't say so
digest must be 32 bytesdigestToFloatsAny other array length

Games

The mapper messages escape from play and verifyRecord as well, when the value came in through params.

MessageThrown byCause
unknown game "<name>"playNot one of the nine names in GAMES. Crash is not one of them
segments must be an integer >= 2wheel1 or less, fractional, or missing when the mapper is called directly
rows must be 8 to 16plinkoA whole number outside the range
plinko with <rows> rows needs <rows> floatsplinkoFloat array shorter than rows. Only reachable by calling the mapper directly
mines must be 1 to 24minesOut of range, fractional or missing
draws must be 1 to 40kenoOut of range or fractional
decks must be 1 to 8deck, blackjack, hiloOut of range or fractional
card index out of rangecardLabelBelow 0, above 51, or fractional
size must be a positive integershuffleZero, negative or fractional size
shuffle of <size> needs <n> floats, got <m>shuffle. Escapes from mines, keno, deck called directlyToo few floats. The need is size - 1

dice, limbo and roulette throw nothing, including for floats outside 0 to 1.

Crash

MessageThrown byCause
chain length must be 1 to 10,000,000createCrashChainOut of range or fractional
game index out of rangecrashGameHashk is not a whole number from 1 to chain.length
game hash must be 64 lowercase hex charscrashResultSame format rule as a server seed
house edge must be in [0, 1)crashResultNegative, or 1 and above. NaN is not caught, and the result is then NaN

verifyCrashLink and expandCrashChain have no messages of their own.

Signing

MessageThrown byCause
secret key must be 128 hex chars (seed || public key)sign, signRecordWrong length, non-hex characters, or capitals

verifySignature and verifyRecordSignature return false for malformed keys and signatures. They don't throw for them.

Canonical JSON and Hex

MessageThrown byCause
canonicalJson: non-finite numbercanonicalJson. Escapes from signingPayload, recordHash, signRecord, verifyRecordSignature, verifyRecordNaN, Infinity or -Infinity anywhere in the value
canonicalJson: unsupported type <type>same as aboveA function, symbol, bigint, or a top-level undefined
fromHex: input must be an even-length hex stringfromHexOdd length or a non-hex character. Capitals are accepted here

inspectRecord and parseInspection

These messages are written for the person who pasted the record, which is why they read differently from the rest. The first five come from parseInspection. inspectRecord runs its input through JSON.stringify and then parseInspection, so it throws them too, with one exception noted in the table.

MessageThrown byCause
Record is too large. Open a JSON file smaller than 64 KB.parseInspection, inspectRecordMore than 65,536 bytes as UTF-8
Record JSON is incomplete or invalid. Include the opening and closing braces.parseInspection, inspectRecordJSON.parse failed. inspectRecord(undefined) lands here too
Record nesting is too deep.parseInspection, inspectRecordMore than 12 levels of objects or arrays
Record numbers must be finite.parseInspectionA literal such as 1e999, which parses to infinity. inspectRecord can't reach it, because JSON.stringify has already turned NaN and Infinity into null
record: expected a JSON object.parseInspection, inspectRecordThe top level is an array, a string, a number or null
Unsupported calculation version. Expected GFS/1.0.inspectRecordspec is not GFS/1.0. A crash record may leave spec out. A single-player record may not
Unsupported profile. Expected single-player or a Crash/Flight record with gameHash.inspectRecordNo gameHash, and profile is not single-player
game: choose one of the nine supported seed-based games.inspectRecordgame is missing, not a string, or not in GAMES
params: expected a JSON object.validateInspectionParams, inspectRecordparams is missing, null, an array or a primitive. {} is fine
params.<key>: not supported for <game>.validateInspectionParams, inspectRecordA key the game doesn't read, such as houseEdge on dice
<field>: enter a whole number from <min> to <max>.validateInspectionParams for segments, rows, mines, draws, decks. inspectRecord for nonce, cursor, atNot a number, not a safe integer, or out of range. The record fields run from 0 to 9007199254740991
houseEdge: expected a number from 0 to 0.5.validateInspectionParams, inspectRecordLimbo's houseEdge is outside the range or not a finite number
clientSeed: expected text.inspectRecordclientSeed is not a string. A string then goes through assertClientSeed
<field>: expected 64 lowercase hexadecimal characters.inspectRecordcommitment, serverSeed, signer, gameHash or previousHash has the wrong format
signature: expected 128 lowercase hexadecimal characters.inspectRecordA single-player record has signature or signer, and signature is malformed or absent
<field>: required for a signed record.inspectRecordA signed record lacks result, cursor, at or commitment
salt: enter 1–1024 characters.inspectRecordCrash record: salt is missing, empty, too long or not a string
houseEdge: expected a number from 0 to 0.999999.inspectRecordCrash record: houseEdge is missing or out of range. There is no default on this path
result: expected a number from 1 to 1.7976931348623157e+308.inspectRecordCrash record: result is present and is not a number of 1 or more

Range messages for parameters name the bare key (rows: ...), while the unsupported-key message has the prefix (params.rows: ...). Don't rely on either form.

Platform Errors

Some failures come from the platform with the platform's wording. A null record or a record with no commitment makes verifyRecord reject with a TypeError. Web Crypto rejects a secret key whose two halves don't match, an empty HMAC key, and a randomBytes request over 65,536 bytes. On a platform without Ed25519, generateKeyPair, sign and verifySignature reject with whatever that platform raises, and inspectRecord reports the Signature check as unsupported.

Reasons Returned by verifyRecord

verifyRecord has two ways of saying no. A record whose fields are well formed but whose values don't add up resolves with ok: false and one string per problem in reasons. A record with a malformed field throws, using the messages above, because the record is replayed through play without being validated first. inspectRecord draws the line more cleanly: it throws for malformed input only, and everything else comes back as checks with a state.

ReasonMeaning
unsupported spec <value>spec is not GFS/1.0
server seed not revealed yet; verify after rotationThe record has no serverSeed. Nothing could be checked, which is not the same as a failed check
server seed does not match commitmentSHA-256 of the seed string differs from commitment
cursor mismatch: computed <n>, record <m>Replaying the inputs used a different highest cursor from the one recorded
result does not match seedsThe replayed result and result differ as canonical JSON
unknown game "<name>"game is not in GAMES. Same text as the error play throws, but returned
signature does not verify under signersignature or signer is present and the pair doesn't verify. A missing half counts. So does a signature made before serverSeed was added to the record

Reasons accumulate. A record can come back with several.

errors-and-reasons.mjs
import { commit, inspectRecord, play, verifyRecord } from '@galabet/fair';

const serverSeed = '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
const { commitment } = await commit(serverSeed);
const record = {
  spec: 'GFS/1.0', profile: 'single-player', game: 'dice', params: {},
  serverSeed, commitment, clientSeed: 'galabet', nonce: 42, cursor: 0, result: 56.12, at: 0,
};

try {
  await play({ game: 'plinko', params: { rows: 7 }, serverSeed, clientSeed: 'galabet', nonce: 42 });
} catch (error) {
  console.log(error instanceof Error, error.code, error.message);
}

const wrong = await verifyRecord({ ...record, spec: 'GFS/2.0', result: 12.34, cursor: 3 });
console.log(wrong.ok, wrong.reasons);

for (const check of [verifyRecord, inspectRecord]) {
  try {
    await check({ ...record, nonce: -1 });
  } catch (error) {
    console.log(`${check.name} threw: ${error.message}`);
  }
}
Output
true undefined rows must be 8 to 16
false [
  'unsupported spec GFS/2.0',
  'cursor mismatch: computed 0, record 3',
  'result does not match seeds'
]
verifyRecord threw: nonce must be a non-negative integer
inspectRecord threw: nonce: enter a whole number from 0 to 9007199254740991.

Wrong values came back as three reasons. The malformed nonce threw from both functions, each with its own wording.