diff --git a/.changeset/three-cats-listen.md b/.changeset/three-cats-listen.md new file mode 100644 index 0000000..987c1a6 --- /dev/null +++ b/.changeset/three-cats-listen.md @@ -0,0 +1,5 @@ +--- +'@clockworklabs/spacetimedb-sdk': minor +--- + +Update Identity and Address to use bigints rather than byte arrays (see https://github.com/clockworklabs/SpacetimeDB/pull/1616) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index d8c6a0f..569abde 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -63,3 +63,11 @@ Given a reducer called `CreatePlayer` you can call it using a call method: ```ts connection.reducers.createPlayer(); ``` + +### Developer notes + +To run the tests, do: + +```sh +pnpm compile && pnpm test +``` diff --git a/packages/sdk/src/address.ts b/packages/sdk/src/address.ts index 512920b..7ac54d5 100644 --- a/packages/sdk/src/address.ts +++ b/packages/sdk/src/address.ts @@ -1,22 +1,24 @@ +import { hexStringToU128, u128ToHexString, u128ToUint8Array } from './utils'; + /** * A unique identifier for a client connected to a database. */ export class Address { - data: Uint8Array; + data: bigint; - get __address_bytes(): Uint8Array { - return this.toUint8Array(); + get __address__(): bigint { + return this.data; } /** * Creates a new `Address`. */ - constructor(data: Uint8Array) { + constructor(data: bigint) { this.data = data; } isZero(): boolean { - return this.data.every(b => b == 0); + return this.data === BigInt(0); } static nullIfZero(addr: Address): Address | null { @@ -28,53 +30,42 @@ export class Address { } static random(): Address { - function randomByte(): number { - return Math.floor(Math.random() * 255); + function randomU8(): number { + return Math.floor(Math.random() * 0xff); } - let data = new Uint8Array(16); + let result = BigInt(0); for (let i = 0; i < 16; i++) { - data[i] = randomByte(); + result = (result << BigInt(8)) | BigInt(randomU8()); } - return new Address(data); + return new Address(result); } /** * Compare two addresses for equality. */ isEqual(other: Address): boolean { - if (this.data.length !== other.data.length) { - return false; - } - for (let i = 0; i < this.data.length; i++) { - if (this.data[i] !== other.data[i]) { - return false; - } - } - return true; + return this.data == other.data; } /** * Print the address as a hexadecimal string. */ toHexString(): string { - return Array.prototype.map - .call(this.data, x => ('00' + x.toString(16)).slice(-2)) - .join(''); + return u128ToHexString(this.data); } + /** + * Convert the address to a Uint8Array. + */ toUint8Array(): Uint8Array { - return this.data; + return u128ToUint8Array(this.data); } /** * Parse an Address from a hexadecimal string. */ static fromString(str: string): Address { - let matches = str.match(/.{1,2}/g) || []; - let data = Uint8Array.from( - matches.map((byte: string) => parseInt(byte, 16)) - ); - return new Address(data); + return new Address(hexStringToU128(str)); } static fromStringOrNull(str: string): Address | null { diff --git a/packages/sdk/src/algebraic_type.ts b/packages/sdk/src/algebraic_type.ts index 19c42b1..c6fb23c 100644 --- a/packages/sdk/src/algebraic_type.ts +++ b/packages/sdk/src/algebraic_type.ts @@ -165,12 +165,12 @@ export class ProductType { deserialize = (reader: BinaryReader): object => { let result: { [key: string]: any } = {}; if (this.elements.length === 1) { - if (this.elements[0].name === '__identity_bytes') { - return new Identity(reader.readUInt8Array()); + if (this.elements[0].name === '__identity__') { + return new Identity(reader.readU256()); } - if (this.elements[0].name === '__address_bytes') { - return new Address(reader.readUInt8Array()); + if (this.elements[0].name === '__address__') { + return new Address(reader.readU128()); } } @@ -318,6 +318,12 @@ export class AlgebraicType { static createU128Type(): AlgebraicType { return this.#createType(Type.U128, null); } + static createI256Type(): AlgebraicType { + return this.#createType(Type.I256, null); + } + static createU256Type(): AlgebraicType { + return this.#createType(Type.U256, null); + } static createF32Type(): AlgebraicType { return this.#createType(Type.F32, null); } @@ -338,12 +344,12 @@ export class AlgebraicType { } static createIdentityType(): AlgebraicType { return this.createProductType([ - new ProductTypeElement('__identity_bytes', this.createBytesType()), + new ProductTypeElement('__identity__', this.createU256Type()), ]); } static createAddressType(): AlgebraicType { return this.createProductType([ - new ProductTypeElement('__address_bytes', this.createBytesType()), + new ProductTypeElement('__address__', this.createU128Type()), ]); } static createScheduleAtType(): AlgebraicType { @@ -377,17 +383,18 @@ export class AlgebraicType { return ( this.isProductType() && this.product.elements.length === 1 && - this.product.elements[0].algebraicType.#isBytes() && + (this.product.elements[0].algebraicType.type == Type.U128 || + this.product.elements[0].algebraicType.type == Type.U256) && this.product.elements[0].name === tag ); } isIdentity(): boolean { - return this.#isBytesNewtype('__identity_bytes'); + return this.#isBytesNewtype('__identity__'); } isAddress(): boolean { - return this.#isBytesNewtype('__address_bytes'); + return this.#isBytesNewtype('__address__'); } serialize(writer: BinaryWriter, value: any): void { @@ -444,6 +451,12 @@ export class AlgebraicType { case Type.U128: writer.writeU128(value); break; + case Type.I256: + writer.writeI256(value); + break; + case Type.U256: + writer.writeU256(value); + break; case Type.F32: writer.writeF32(value); break; @@ -530,6 +543,8 @@ export namespace AlgebraicType { U64 = 'U64', I128 = 'I128', U128 = 'U128', + I256 = 'I256', + U256 = 'U256', F32 = 'F32', F64 = 'F64', /** UTF-8 encoded */ diff --git a/packages/sdk/src/algebraic_value.ts b/packages/sdk/src/algebraic_value.ts index 8ff9ce9..ca41c03 100644 --- a/packages/sdk/src/algebraic_value.ts +++ b/packages/sdk/src/algebraic_value.ts @@ -264,6 +264,9 @@ export class AlgebraicValue { } } + // TODO: all of the following methods should actually check the type of `self.value` + // and throw if it does not match. + asProductValue(): ProductValue { return this.value as ProductValue; } @@ -305,11 +308,11 @@ export class AlgebraicValue { } asIdentity(): Identity { - return new Identity(this.asField(0).asBytes()); + return new Identity(this.asField(0).asBigInt()); } asAddress(): Address { - return new Address(this.asField(0).asBytes()); + return new Address(this.asField(0).asBigInt()); } } diff --git a/packages/sdk/src/binary_reader.ts b/packages/sdk/src/binary_reader.ts index 05f6f12..f40f410 100644 --- a/packages/sdk/src/binary_reader.ts +++ b/packages/sdk/src/binary_reader.ts @@ -101,13 +101,43 @@ export default class BinaryReader { } readI128(): bigint { - const lowerPart = this.#buffer.getBigInt64(this.#offset, true); + const lowerPart = this.#buffer.getBigUint64(this.#offset, true); const upperPart = this.#buffer.getBigInt64(this.#offset + 8, true); this.#offset += 16; return (upperPart << BigInt(64)) + lowerPart; } + readU256(): bigint { + const p0 = this.#buffer.getBigUint64(this.#offset, true); + const p1 = this.#buffer.getBigUint64(this.#offset + 8, true); + const p2 = this.#buffer.getBigUint64(this.#offset + 16, true); + const p3 = this.#buffer.getBigUint64(this.#offset + 24, true); + this.#offset += 32; + + return ( + (p3 << BigInt(3 * 64)) + + (p2 << BigInt(2 * 64)) + + (p1 << BigInt(1 * 64)) + + p0 + ); + } + + readI256(): bigint { + const p0 = this.#buffer.getBigUint64(this.#offset, true); + const p1 = this.#buffer.getBigUint64(this.#offset + 8, true); + const p2 = this.#buffer.getBigUint64(this.#offset + 16, true); + const p3 = this.#buffer.getBigInt64(this.#offset + 24, true); + this.#offset += 32; + + return ( + (p3 << BigInt(3 * 64)) + + (p2 << BigInt(2 * 64)) + + (p1 << BigInt(1 * 64)) + + p0 + ); + } + readF32(): number { const value = this.#buffer.getFloat32(this.#offset, true); this.#offset += 4; diff --git a/packages/sdk/src/binary_writer.ts b/packages/sdk/src/binary_writer.ts index 2c2a679..b579bfd 100644 --- a/packages/sdk/src/binary_writer.ts +++ b/packages/sdk/src/binary_writer.ts @@ -111,6 +111,34 @@ export default class BinaryWriter { this.#offset += 16; } + writeU256(value: bigint): void { + this.#expandBuffer(32); + const low_64_mask = BigInt('0xFFFFFFFFFFFFFFFF'); + const p0 = value & low_64_mask; + const p1 = (value >> BigInt(64 * 1)) & low_64_mask; + const p2 = (value >> BigInt(64 * 2)) & low_64_mask; + const p3 = value >> BigInt(64 * 3); + this.#view.setBigUint64(this.#offset + 8 * 0, p0, true); + this.#view.setBigUint64(this.#offset + 8 * 1, p1, true); + this.#view.setBigUint64(this.#offset + 8 * 2, p2, true); + this.#view.setBigUint64(this.#offset + 8 * 3, p3, true); + this.#offset += 32; + } + + writeI256(value: bigint): void { + this.#expandBuffer(32); + const low_64_mask = BigInt('0xFFFFFFFFFFFFFFFF'); + const p0 = value & low_64_mask; + const p1 = (value >> BigInt(64 * 1)) & low_64_mask; + const p2 = (value >> BigInt(64 * 2)) & low_64_mask; + const p3 = value >> BigInt(64 * 3); + this.#view.setBigUint64(this.#offset + 8 * 0, p0, true); + this.#view.setBigUint64(this.#offset + 8 * 1, p1, true); + this.#view.setBigUint64(this.#offset + 8 * 2, p2, true); + this.#view.setBigInt64(this.#offset + 8 * 3, p3, true); + this.#offset += 32; + } + writeF32(value: number): void { this.#expandBuffer(4); this.#view.setFloat32(this.#offset, value, true); diff --git a/packages/sdk/src/identity.ts b/packages/sdk/src/identity.ts index 35a6888..9ce25dc 100644 --- a/packages/sdk/src/identity.ts +++ b/packages/sdk/src/identity.ts @@ -1,37 +1,26 @@ -// Helper function convert from string to Uint8Array -function hexStringToUint8Array(str: string): Uint8Array { - let matches = str.match(/.{1,2}/g) || []; - let data = Uint8Array.from(matches.map((byte: string) => parseInt(byte, 16))); - return data; -} - -// Helper function for converting Uint8Array to hex string -function uint8ArrayToHexString(array: Uint8Array): string { - return Array.prototype.map - .call(array, x => ('00' + x.toString(16)).slice(-2)) - .join(''); -} +import BinaryReader from './binary_reader'; +import BinaryWriter from './binary_writer'; +import { hexStringToU256, u256ToHexString, u256ToUint8Array } from './utils'; /** * A unique identifier for a user connected to a database. */ export class Identity { - data: Uint8Array; + data: bigint; - get __identity_bytes(): Uint8Array { - return this.toUint8Array(); + get __identity__(): bigint { + return this.data; } /** * Creates a new `Identity`. + * + * `data` can be a hexadecimal string or a `bigint`. */ - constructor(data: string | Uint8Array) { - // we get a JSON with __identity_bytes when getting a token with a JSON API - // and an Uint8Array when using BSATN - this.data = - data.constructor === Uint8Array - ? data - : hexStringToUint8Array(data as string); + 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.data = typeof data === 'string' ? hexStringToU256(data) : data; } /** @@ -45,11 +34,14 @@ export class Identity { * Print the identity as a hexadecimal string. */ toHexString(): string { - return uint8ArrayToHexString(this.data); + return u256ToHexString(this.data); } + /** + * Convert the address to a Uint8Array. + */ toUint8Array(): Uint8Array { - return this.data; + return u256ToUint8Array(this.data); } /** diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 9eb42a8..75c6255 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,3 +1,6 @@ +import BinaryReader from './binary_reader'; +import BinaryWriter from './binary_writer'; + export function toPascalCase(s: string): string { const str = s.replace(/([-_][a-z])/gi, $1 => { return $1.toUpperCase().replace('-', '').replace('_', ''); @@ -36,3 +39,63 @@ export function deepEqual(obj1: any, obj2: any): boolean { return true; } + +export function uint8ArrayToHexString(array: Uint8Array): string { + return Array.prototype.map + .call(array, x => ('00' + x.toString(16)).slice(-2)) + .join(''); +} + +export function uint8ArrayToU128(array: Uint8Array): bigint { + if (array.length != 16) { + throw new Error(`Uint8Array is not 16 bytes long: ${array}`); + } + return new BinaryReader(array).readU128(); +} + +export function uint8ArrayToU256(array: Uint8Array): bigint { + if (array.length != 32) { + throw new Error(`Uint8Array is not 32 bytes long: [${array}]`); + } + return new BinaryReader(array).readU256(); +} + +export function hexStringToUint8Array(str: string): Uint8Array { + if (str.startsWith('0x')) { + str = str.slice(2); + } + let matches = str.match(/.{1,2}/g) || []; + let data = Uint8Array.from(matches.map((byte: string) => parseInt(byte, 16))); + if (data.length != 32) { + return new Uint8Array(0); + } + return data; +} + +export function hexStringToU128(str: string): bigint { + return uint8ArrayToU128(hexStringToUint8Array(str)); +} + +export function hexStringToU256(str: string): bigint { + return uint8ArrayToU256(hexStringToUint8Array(str)); +} + +export function u128ToUint8Array(data: bigint): Uint8Array { + let writer = new BinaryWriter(16); + writer.writeU128(data); + return writer.getBuffer(); +} + +export function u128ToHexString(data: bigint): string { + return uint8ArrayToHexString(u128ToUint8Array(data)); +} + +export function u256ToUint8Array(data: bigint): Uint8Array { + let writer = new BinaryWriter(32); + writer.writeU256(data); + return writer.getBuffer(); +} + +export function u256ToHexString(data: bigint): string { + return uint8ArrayToHexString(u256ToUint8Array(data)); +} diff --git a/packages/sdk/tests/binary_read_write.test.ts b/packages/sdk/tests/binary_read_write.test.ts new file mode 100644 index 0000000..993f667 --- /dev/null +++ b/packages/sdk/tests/binary_read_write.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from 'vitest'; +import BinaryReader from '../src/binary_reader'; +import BinaryWriter from '../src/binary_writer'; + +/* +// Generated by the following Rust code: + +#[cfg(test)] +mod tests { + use rand::{thread_rng, Rng}; + use spacetimedb_sats::{bsatn, i256, u256}; + + #[test] + fn make_some_numbers() { + let mut rng = thread_rng(); + let v_u8: u8 = rng.gen(); + let v_u16: u16 = rng.gen(); + let v_u32: u32 = rng.gen(); + let v_u64: u64 = rng.gen(); + let v_u128: u128 = rng.gen(); + let v_u256: u256 = u256::from_words(rng.gen(), rng.gen()); + let v_i8: i8 = rng.gen(); + let v_i16: i16 = rng.gen(); + let v_i32: i32 = rng.gen(); + let v_i64: i64 = rng.gen(); + let v_i128: i128 = rng.gen(); + let v_i256: i256 = i256::from_words(rng.gen(), rng.gen()); + + println!("['I8', {}, {:?}],", v_i8, bsatn::to_vec(&v_i8).unwrap()); + println!("['I16', {}, {:?}],", v_i16, bsatn::to_vec(&v_i16).unwrap()); + println!("['I32', {}, {:?}],", v_i32, bsatn::to_vec(&v_i32).unwrap()); + println!("['I64', BigInt('{}'), {:?}],", v_i64, bsatn::to_vec(&v_i64).unwrap()); + println!("['I128', BigInt('{}'), {:?}],", v_i128, bsatn::to_vec(&v_i128).unwrap()); + println!("['I256', BigInt('{}'), {:?}],", v_i256, bsatn::to_vec(&v_i256).unwrap()); + + println!("['U8', {}, {:?}],", v_u8, bsatn::to_vec(&v_u8).unwrap()); + println!("['U16', {}, {:?}],", v_u16, bsatn::to_vec(&v_u16).unwrap()); + println!("['U32', {}, {:?}],", v_u32, bsatn::to_vec(&v_u32).unwrap()); + println!("['U64', BigInt('{}'), {:?}],", v_u64, bsatn::to_vec(&v_u64).unwrap()); + println!("['U128', BigInt('{}'), {:?}],", v_u128, bsatn::to_vec(&v_u128).unwrap()); + println!("['U256', BigInt('{}'), {:?}],", v_u256, bsatn::to_vec(&v_u256).unwrap()); + panic!(); + } +} +*/ + +let testCases: Array<[string, BigInt | number, Array]> = [ + ['I8', 48, [48]], + ['I16', 2910, [94, 11]], + ['I32', -799760706, [190, 158, 84, 208]], + [ + 'I64', + BigInt('-1541553498090056195'), + [253, 213, 20, 208, 66, 77, 155, 234], + ], + [ + 'I128', + BigInt('12547586996680216771838914786222604020'), + [244, 254, 202, 102, 17, 36, 114, 210, 182, 88, 120, 98, 205, 147, 112, 9], + ], + [ + 'I256', + BigInt( + '35334490670013506332541201493144667192747188790291257662501378603950330458369' + ), + [ + 1, 177, 117, 147, 65, 153, 110, 71, 110, 80, 45, 231, 208, 112, 149, 150, + 251, 157, 51, 25, 129, 124, 13, 154, 238, 225, 7, 63, 237, 156, 30, 78, + ], + ], + ['U8', 63, [63]], + ['U16', 14776, [184, 57]], + ['U32', 2260346643, [19, 39, 186, 134]], + [ + 'U64', + BigInt('6355943419584016569'), + [185, 112, 201, 104, 221, 216, 52, 88], + ], + [ + 'U128', + BigInt('190443100270131819986139062814080853012'), + [20, 100, 201, 134, 99, 82, 196, 32, 34, 79, 25, 142, 199, 1, 70, 143], + ], + [ + 'U256', + BigInt( + '58716185326733447174109779681509939791568291171619953995835894271369692835957' + ), + [ + 117, 240, 99, 239, 213, 99, 55, 201, 2, 145, 4, 24, 0, 173, 62, 27, 124, + 53, 44, 244, 71, 1, 156, 30, 111, 187, 149, 150, 229, 46, 208, 129, + ], + ], +]; + +describe('BinaryReader/Writer', () => { + test('correctly reads/writes little endian values', () => { + for (let [name, int, buf] of testCases) { + let arr = new Uint8Array(buf); + let reader = new BinaryReader(arr); + + let read = reader['read' + name](); + expect(read).toEqual(int); + + let writer = new BinaryWriter(0); + writer['write' + name](int); + + expect(writer.getBuffer()).toEqual(arr); + } + }); +}); diff --git a/packages/sdk/tests/spacetimedb_client.test.ts b/packages/sdk/tests/spacetimedb_client.test.ts index 6a4d0a7..81c3c33 100644 --- a/packages/sdk/tests/spacetimedb_client.test.ts +++ b/packages/sdk/tests/spacetimedb_client.test.ts @@ -15,6 +15,16 @@ import { ReducerEvent } from '../src/db_connection_impl'; import { Identity } from '../src/identity'; import WebsocketTestAdapter from '../src/websocket_test_adapter'; +const anIdentity = Identity.fromString( + '0000000000000000000000000000000000000000000000000000000000000069' +); +const bobIdentity = Identity.fromString( + '0000000000000000000000000000000000000000000000000000000000000b0b' +); +const sallyIdentity = Identity.fromString( + '000000000000000000000000000000000000000000000000000000000006a111' +); + class Deferred { #isResolved: boolean = false; #isRejected: boolean = false; @@ -138,7 +148,7 @@ describe('SpacetimeDBClient', () => { wsAdapter.acceptConnection(); const tokenMessage = ws.ServerMessage.IdentityToken({ - identity: new Identity('an-identity'), + identity: anIdentity, token: 'a-token', address: Address.random(), }); @@ -165,7 +175,7 @@ describe('SpacetimeDBClient', () => { wsAdapter.acceptConnection(); const tokenMessage = ws.ServerMessage.IdentityToken({ - identity: new Identity('an-identity'), + identity: anIdentity, token: 'a-token', address: Address.random(), }); @@ -275,7 +285,7 @@ describe('SpacetimeDBClient', () => { ], }), timestamp: { microseconds: BigInt(1681391805281203) }, - callerIdentity: new Identity('00ff01'), + callerIdentity: anIdentity, callerAddress: Address.random(), reducerCall: { reducerName: 'create_player', @@ -294,9 +304,7 @@ describe('SpacetimeDBClient', () => { expect(inserts[1].player.ownerId).toBe('player-2'); expect(inserts[1].reducerEvent?.reducer.name).toBe('create_player'); expect(inserts[1].reducerEvent?.status.tag).toBe('Committed'); - expect(inserts[1].reducerEvent?.callerIdentity).toEqual( - Identity.fromString('00ff01') - ); + expect(inserts[1].reducerEvent?.callerIdentity).toEqual(anIdentity); expect(inserts[1].reducerEvent?.reducer.args).toEqual({ name: 'A Player', location: { x: 2, y: 3 }, @@ -305,7 +313,7 @@ describe('SpacetimeDBClient', () => { expect(reducerCallbackLog).toHaveLength(1); expect(reducerCallbackLog[0].reducerEvent.callerIdentity).toEqual( - Identity.fromString('00ff01') + anIdentity ); }); @@ -325,7 +333,7 @@ describe('SpacetimeDBClient', () => { wsAdapter.acceptConnection(); const tokenMessage = ws.ServerMessage.IdentityToken({ - identity: new Identity('an-identity'), + identity: anIdentity, token: 'a-token', address: Address.random(), }); @@ -425,7 +433,7 @@ describe('SpacetimeDBClient', () => { ], }), timestamp: { microseconds: BigInt(1681391805281203) }, - callerIdentity: new Identity('00ff01'), + callerIdentity: anIdentity, callerAddress: Address.random(), reducerCall: { reducerName: 'create_player', @@ -507,7 +515,7 @@ describe('SpacetimeDBClient', () => { ], }), timestamp: { microseconds: BigInt(1681391805281203) }, - callerIdentity: new Identity('00ff01'), + callerIdentity: anIdentity, callerAddress: Address.random(), reducerCall: { reducerName: 'create_player', @@ -541,7 +549,9 @@ describe('SpacetimeDBClient', () => { wsAdapter.acceptConnection(); const tokenMessage = ws.ServerMessage.IdentityToken({ - identity: new Identity('an-identity'), + identity: Identity.fromString( + '0000000000000000000000000000000000000000000000000000000000000069' + ), token: 'a-token', address: Address.random(), }); @@ -652,7 +662,7 @@ describe('SpacetimeDBClient', () => { ], }), timestamp: { microseconds: BigInt(1681391805281203) }, - callerIdentity: new Identity('00ff01'), + callerIdentity: anIdentity, callerAddress: Address.random(), reducerCall: { reducerName: 'create_player', @@ -682,18 +692,16 @@ describe('SpacetimeDBClient', () => { .build(); await client.wsPromise; const db = client.db; - const user1 = { identity: new Identity('bobs-idenitty'), username: 'bob' }; + const user1 = { identity: bobIdentity, username: 'bob' }; const user2 = { - identity: new Identity('sallys-identity'), + identity: sallyIdentity, username: 'sally', }; const users: Map = (db.user.tableCache as any).rows; users.set('abc123', user1); users.set('def456', user2); - const filteredUser = client.db.user.identity.find( - new Identity('sallys-identity') - ); + const filteredUser = client.db.user.identity.find(sallyIdentity); expect(filteredUser).not.toBeUndefined; expect(filteredUser!.username).toBe('sally'); }); diff --git a/packages/test-app/src/App.tsx b/packages/test-app/src/App.tsx index 0988638..2fa9a35 100644 --- a/packages/test-app/src/App.tsx +++ b/packages/test-app/src/App.tsx @@ -24,9 +24,9 @@ function App() { }) .withCredentials([ Identity.fromString( - '93dda09db9a56d8fa6c024d843e805d8262191db3b4ba84c5efcd1ad451fed4e' + 'c200f95df78dfd9cf328791c9fa8dcd60525d7fe361e29cf13454a6c71d91ef1' ), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiI5M2RkYTA5ZGI5YTU2ZDhmYTZjMDI0ZDg0M2U4MDVkODI2MjE5MWRiM2I0YmE4NGM1ZWZjZDFhZDQ1MWZlZDRlIiwiaWF0IjoxNzI4Mzc5MjE2LCJleHAiOm51bGx9.dKanuJu7xKg_g3toOBO09Po3ZgxnHnUwZYpwbEwjrHWGkRzNSL9sLRNjKatUR7OXmwd9b0pCTray4GUt0VlCGg', + 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMUpCQkNIV0pWMUpHNVg4Uk42S1E5N05GUCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImlhdCI6MTczMDQ2NDgxNywiZXhwIjoxNzkzNTM2ODE3fQ.aQjAg_aIa5UTr3CFTJY06_TtNLsCya_JXA3zfPlgeUm4DNXlFiTpRqnDtAtSRrArAb3WNk5LRX3XVuu23ulZzUZfq9tnHpG3ogd8-8ZmjtHB7mIAbaHUsKQs5cKRPrjvMvg6-hUdLnbLqBuMz4l2A1kl9d-XyYExXcZSl3GvvwkfoxxDAZkB7GVX557EofKCT-w8NCa3HE-1d9PEeQneVRwKh1pEKFtJcXGVAdppnp5fDTtjUKXk4uTdvWRK_psZRitwSDfE2Ikuna95c1_dtxG1MTfGQF6QyI5aHpZYnWYVtwikPZ87XiRVE41hNmQmuv9fbeG6UHNsWM7MeBA4yg', ]) .build() );