Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/bindings-typescript/src/lib/connection_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
11 changes: 9 additions & 2 deletions crates/bindings-typescript/src/lib/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion crates/bindings-typescript/src/lib/time_duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion crates/bindings-typescript/src/lib/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions crates/bindings-typescript/src/lib/uuid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
72 changes: 72 additions & 0 deletions crates/bindings-typescript/tests/id_ctor_coercion.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});