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
| Message | Thrown by | Cause |
|---|---|---|
server seed must be 64 lowercase hex characters | assertServerSeed. Escapes from commit, verifyCommitment, deriveDigest, deriveFloats, play, verifyRecord | Wrong length, a character outside 0-9a-f, capital letters, or not a string |
client seed must be 1 to 64 characters | assertClientSeed. Escapes from deriveDigest, deriveFloats, play, verifyRecord, inspectRecord | Empty, longer than 64 UTF-16 code units, or not a string |
client seed must not contain ":" (reserved as the HMAC message separator) | same as above | A colon anywhere in the seed |
Derivation
| Message | Thrown by | Cause |
|---|---|---|
nonce must be a non-negative integer | deriveDigest. Escapes from deriveFloats, play, verifyRecord | Negative, fractional, NaN, or a string such as "42" |
cursor must be a non-negative integer | deriveDigest | Same rule, for the second argument |
count must be a positive integer | deriveFloats. Escapes from play, verifyRecord | Zero, 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 bytes | digestToFloats | Any other array length |
Games
The mapper messages escape from play and verifyRecord as well, when the value came in through params.
| Message | Thrown by | Cause |
|---|---|---|
unknown game "<name>" | play | Not one of the nine names in GAMES. Crash is not one of them |
segments must be an integer >= 2 | wheel | 1 or less, fractional, or missing when the mapper is called directly |
rows must be 8 to 16 | plinko | A whole number outside the range |
plinko with <rows> rows needs <rows> floats | plinko | Float array shorter than rows. Only reachable by calling the mapper directly |
mines must be 1 to 24 | mines | Out of range, fractional or missing |
draws must be 1 to 40 | keno | Out of range or fractional |
decks must be 1 to 8 | deck, blackjack, hilo | Out of range or fractional |
card index out of range | cardLabel | Below 0, above 51, or fractional |
size must be a positive integer | shuffle | Zero, negative or fractional size |
shuffle of <size> needs <n> floats, got <m> | shuffle. Escapes from mines, keno, deck called directly | Too few floats. The need is size - 1 |
dice, limbo and roulette throw nothing, including for floats outside 0 to 1.
Crash
| Message | Thrown by | Cause |
|---|---|---|
chain length must be 1 to 10,000,000 | createCrashChain | Out of range or fractional |
game index out of range | crashGameHash | k is not a whole number from 1 to chain.length |
game hash must be 64 lowercase hex chars | crashResult | Same format rule as a server seed |
house edge must be in [0, 1) | crashResult | Negative, or 1 and above. NaN is not caught, and the result is then NaN |
verifyCrashLink and expandCrashChain have no messages of their own.
Signing
| Message | Thrown by | Cause |
|---|---|---|
secret key must be 128 hex chars (seed || public key) | sign, signRecord | Wrong 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
| Message | Thrown by | Cause |
|---|---|---|
canonicalJson: non-finite number | canonicalJson. Escapes from signingPayload, recordHash, signRecord, verifyRecordSignature, verifyRecord | NaN, Infinity or -Infinity anywhere in the value |
canonicalJson: unsupported type <type> | same as above | A function, symbol, bigint, or a top-level undefined |
fromHex: input must be an even-length hex string | fromHex | Odd 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.
| Message | Thrown by | Cause |
|---|---|---|
Record is too large. Open a JSON file smaller than 64 KB. | parseInspection, inspectRecord | More than 65,536 bytes as UTF-8 |
Record JSON is incomplete or invalid. Include the opening and closing braces. | parseInspection, inspectRecord | JSON.parse failed. inspectRecord(undefined) lands here too |
Record nesting is too deep. | parseInspection, inspectRecord | More than 12 levels of objects or arrays |
Record numbers must be finite. | parseInspection | A 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, inspectRecord | The top level is an array, a string, a number or null |
Unsupported calculation version. Expected GFS/1.0. | inspectRecord | spec 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. | inspectRecord | No gameHash, and profile is not single-player |
game: choose one of the nine supported seed-based games. | inspectRecord | game is missing, not a string, or not in GAMES |
params: expected a JSON object. | validateInspectionParams, inspectRecord | params is missing, null, an array or a primitive. {} is fine |
params.<key>: not supported for <game>. | validateInspectionParams, inspectRecord | A 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, at | Not 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, inspectRecord | Limbo's houseEdge is outside the range or not a finite number |
clientSeed: expected text. | inspectRecord | clientSeed is not a string. A string then goes through assertClientSeed |
<field>: expected 64 lowercase hexadecimal characters. | inspectRecord | commitment, serverSeed, signer, gameHash or previousHash has the wrong format |
signature: expected 128 lowercase hexadecimal characters. | inspectRecord | A single-player record has signature or signer, and signature is malformed or absent |
<field>: required for a signed record. | inspectRecord | A signed record lacks result, cursor, at or commitment |
salt: enter 1–1024 characters. | inspectRecord | Crash record: salt is missing, empty, too long or not a string |
houseEdge: expected a number from 0 to 0.999999. | inspectRecord | Crash 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. | inspectRecord | Crash 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.
| Reason | Meaning |
|---|---|
unsupported spec <value> | spec is not GFS/1.0 |
server seed not revealed yet; verify after rotation | The record has no serverSeed. Nothing could be checked, which is not the same as a failed check |
server seed does not match commitment | SHA-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 seeds | The 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 signer | signature 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.
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}`);
}
}
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.
