diff --git a/crates/bindings-typescript/src/lib/connection_id.ts b/crates/bindings-typescript/src/lib/connection_id.ts index a32bbdf4f14..4c8e8042fe8 100644 --- a/crates/bindings-typescript/src/lib/connection_id.ts +++ b/crates/bindings-typescript/src/lib/connection_id.ts @@ -18,7 +18,10 @@ export class ConnectionId { * Creates a new `ConnectionId`. */ constructor(data: bigint) { - this.__connection_id__ = data; + // Coerce through BigInt() so callers who arrive via JSON (e.g. HTTP + // responses or custom state caches that lose bigint precision) get + // a clear early failure instead of silent field corruption. + this.__connection_id__ = BigInt(data); } /** diff --git a/crates/bindings-typescript/src/lib/identity.ts b/crates/bindings-typescript/src/lib/identity.ts index 6cad2761b5e..d3b894fbeca 100644 --- a/crates/bindings-typescript/src/lib/identity.ts +++ b/crates/bindings-typescript/src/lib/identity.ts @@ -21,8 +21,15 @@ export class Identity { */ constructor(data: string | bigint) { // we get a JSON with __identity__ when getting a token with a JSON API - // and an bigint when using BSATN - this.__identity__ = typeof data === 'string' ? hexStringToU256(data) : data; + // and an bigint when using BSATN. + // Coerce non-string inputs through BigInt(): callers that go via any + // JSON path outside the SDK (custom state caches, hand-rolled HTTP + // clients, etc.) can end up with `number` for a u256 field, which + // would otherwise be stored verbatim and crash later in BSATN + // serialization with an opaque "Cannot mix BigInt and other types" + // error. + this.__identity__ = + typeof data === 'string' ? hexStringToU256(data) : BigInt(data); } /** diff --git a/crates/bindings-typescript/src/lib/time_duration.ts b/crates/bindings-typescript/src/lib/time_duration.ts index 15d6141300e..41a2b67bcd1 100644 --- a/crates/bindings-typescript/src/lib/time_duration.ts +++ b/crates/bindings-typescript/src/lib/time_duration.ts @@ -58,7 +58,10 @@ export class TimeDuration { } constructor(micros: bigint) { - this.__time_duration_micros__ = micros; + // Coerce through BigInt() so callers who arrive via JSON (where + // bigint precision is lost) get a clear early failure instead of + // silent field corruption that crashes later in arithmetic. + this.__time_duration_micros__ = BigInt(micros); } static fromMillis(millis: number): TimeDuration { diff --git a/crates/bindings-typescript/src/lib/timestamp.ts b/crates/bindings-typescript/src/lib/timestamp.ts index adc099bfeb7..759969ab5d1 100644 --- a/crates/bindings-typescript/src/lib/timestamp.ts +++ b/crates/bindings-typescript/src/lib/timestamp.ts @@ -26,7 +26,10 @@ export class Timestamp { } constructor(micros: bigint) { - this.__timestamp_micros_since_unix_epoch__ = micros; + // Coerce through BigInt() so callers who arrive via JSON (where + // bigint precision is lost) get a clear early failure instead of + // silent field corruption that crashes later in arithmetic. + this.__timestamp_micros_since_unix_epoch__ = BigInt(micros); } /** diff --git a/crates/bindings-typescript/src/lib/uuid.ts b/crates/bindings-typescript/src/lib/uuid.ts index 2216e1e9499..d30df26be1f 100644 --- a/crates/bindings-typescript/src/lib/uuid.ts +++ b/crates/bindings-typescript/src/lib/uuid.ts @@ -67,11 +67,15 @@ export class Uuid { * @throws {Error} If the value is outside the valid UUID range */ constructor(u: bigint) { + // Coerce through BigInt() so callers who arrive via JSON (where + // bigint precision is lost) hit the range check rather than a + // cryptic `Cannot mix BigInt and other types` error. + const v = BigInt(u); // Must fit in exactly 16 bytes - if (u < 0n || u > Uuid.MAX_UUID_BIGINT) { + if (v < 0n || v > Uuid.MAX_UUID_BIGINT) { throw new Error('Invalid UUID: must be between 0 and `MAX_UUID_BIGINT`'); } - this.__uuid__ = u; + this.__uuid__ = v; } /** diff --git a/crates/bindings-typescript/tests/id_ctor_coercion.test.ts b/crates/bindings-typescript/tests/id_ctor_coercion.test.ts new file mode 100644 index 00000000000..ad7083b8c46 --- /dev/null +++ b/crates/bindings-typescript/tests/id_ctor_coercion.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from 'vitest'; +import { ConnectionId, Identity, TimeDuration, Timestamp, Uuid } from '../src'; + +// The constructors of these wrapper classes claim to take `bigint` (or +// `string | bigint` for Identity), but real-world callers can end up +// passing `number` — typically after data goes through JSON in some +// external layer (HTTP responses, state caches, custom serializers), +// since JSON has no `bigint` and `JSON.parse` produces `number`. Before +// the coercion patch this silently corrupted the field and crashed +// later during BSATN serialization with an opaque mix-error. These +// tests pin the new behavior: numeric inputs are coerced to bigint and +// the field is always a bigint. + +describe('id-like constructors coerce non-bigint numerics', () => { + test('Identity(number) stores a bigint', () => { + const id = new Identity(123 as unknown as bigint); + expect(typeof id.__identity__).toBe('bigint'); + expect(id.__identity__).toBe(123n); + }); + + test('Identity(bigint) stays bigint', () => { + const id = new Identity(123n); + expect(typeof id.__identity__).toBe('bigint'); + expect(id.__identity__).toBe(123n); + }); + + test('Identity(hex string) still parses', () => { + const id = new Identity( + '0x0000000000000000000000000000000000000000000000000000000000000001' + ); + expect(typeof id.__identity__).toBe('bigint'); + expect(id.__identity__).toBe(1n); + }); + + test('ConnectionId(number) stores a bigint', () => { + const c = new ConnectionId(42 as unknown as bigint); + expect(typeof c.__connection_id__).toBe('bigint'); + expect(c.__connection_id__).toBe(42n); + // Downstream consumers rely on bigint operators working: + expect(c.isZero()).toBe(false); + expect(c.toHexString().length).toBeGreaterThan(0); + }); + + test('Timestamp(number) stores a bigint', () => { + const t = new Timestamp(1_000_000 as unknown as bigint); + expect(typeof t.__timestamp_micros_since_unix_epoch__).toBe('bigint'); + expect(t.microsSinceUnixEpoch).toBe(1_000_000n); + // toDate() does bigint arithmetic — would throw mix-error before. + expect(t.toDate().getTime()).toBe(1000); + }); + + test('TimeDuration(number) stores a bigint', () => { + const d = new TimeDuration(5_000_000 as unknown as bigint); + expect(typeof d.__time_duration_micros__).toBe('bigint'); + expect(d.micros).toBe(5_000_000n); + }); + + test('Uuid(number) is coerced and range-checked', () => { + const u = new Uuid(0 as unknown as bigint); + expect(typeof u.__uuid__).toBe('bigint'); + expect(u.__uuid__).toBe(0n); + }); + + test('Uuid(out-of-range bigint) still throws', () => { + expect(() => new Uuid(Uuid.MAX_UUID_BIGINT + 1n)).toThrow(/Invalid UUID/); + expect(() => new Uuid(-1n)).toThrow(/Invalid UUID/); + }); + + test('Identity(undefined) throws a clear error instead of corrupting', () => { + expect(() => new Identity(undefined as unknown as bigint)).toThrow(); + }); +});