(wasm) Treat arbitrary byte data as Uint8Array, instead of base64url marshalling.
This commit is contained in:
		
							
								
								
									
										2
									
								
								veilid-wasm/tests/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								veilid-wasm/tests/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,2 @@
 | 
			
		||||
node_modules
 | 
			
		||||
veilid-wasm-pkg
 | 
			
		||||
coverage
 | 
			
		||||
							
								
								
									
										173
									
								
								veilid-wasm/tests/src/VeilidRoutingContext.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								veilid-wasm/tests/src/VeilidRoutingContext.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
import { expect } from '@wdio/globals';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  veilidCoreInitConfig,
 | 
			
		||||
  veilidCoreStartupConfig,
 | 
			
		||||
} from './utils/veilid-config';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  DHTRecordDescriptor,
 | 
			
		||||
  VeilidRoutingContext,
 | 
			
		||||
  veilidClient,
 | 
			
		||||
  veilidCrypto,
 | 
			
		||||
} from 'veilid-wasm';
 | 
			
		||||
import { textEncoder, textDecoder } from './utils/marshalling-utils';
 | 
			
		||||
import { waitForMs } from './utils/wait-utils';
 | 
			
		||||
 | 
			
		||||
describe('VeilidRoutingContext', () => {
 | 
			
		||||
  before('veilid startup', async () => {
 | 
			
		||||
    veilidClient.initializeCore(veilidCoreInitConfig);
 | 
			
		||||
    await veilidClient.startupCore((_update) => {
 | 
			
		||||
      // if (_update.kind === 'Log') {
 | 
			
		||||
      //   console.log(_update.message);
 | 
			
		||||
      // }
 | 
			
		||||
    }, JSON.stringify(veilidCoreStartupConfig));
 | 
			
		||||
    await veilidClient.attach();
 | 
			
		||||
    await waitForMs(2000);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  after('veilid shutdown', async () => {
 | 
			
		||||
    await veilidClient.detach();
 | 
			
		||||
    await veilidClient.shutdownCore();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('constructors', () => {
 | 
			
		||||
    it('should create using .create()', async () => {
 | 
			
		||||
      const routingContext = VeilidRoutingContext.create();
 | 
			
		||||
      expect(routingContext instanceof VeilidRoutingContext).toBe(true);
 | 
			
		||||
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create using new', async () => {
 | 
			
		||||
      const routingContext = new VeilidRoutingContext();
 | 
			
		||||
      expect(routingContext instanceof VeilidRoutingContext).toBe(true);
 | 
			
		||||
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create with privacy', async () => {
 | 
			
		||||
      const routingContext = VeilidRoutingContext.create().withPrivacy();
 | 
			
		||||
      expect(routingContext instanceof VeilidRoutingContext).toBe(true);
 | 
			
		||||
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create with custom privacy', async () => {
 | 
			
		||||
      const routingContext = VeilidRoutingContext.create().withCustomPrivacy({
 | 
			
		||||
        Safe: {
 | 
			
		||||
          hop_count: 2,
 | 
			
		||||
          sequencing: 'EnsureOrdered',
 | 
			
		||||
          stability: 'Reliable',
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      expect(routingContext instanceof VeilidRoutingContext).toBe(true);
 | 
			
		||||
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create with sequencing', async () => {
 | 
			
		||||
      const routingContext =
 | 
			
		||||
        VeilidRoutingContext.create().withSequencing('EnsureOrdered');
 | 
			
		||||
      expect(routingContext instanceof VeilidRoutingContext).toBe(true);
 | 
			
		||||
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('operations', () => {
 | 
			
		||||
    let routingContext: VeilidRoutingContext;
 | 
			
		||||
 | 
			
		||||
    before('create routing context', () => {
 | 
			
		||||
      routingContext = VeilidRoutingContext.create()
 | 
			
		||||
        .withPrivacy()
 | 
			
		||||
        .withSequencing('EnsureOrdered');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    after('free routing context', () => {
 | 
			
		||||
      routingContext.free();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('DHT kitchen sink', async () => {
 | 
			
		||||
      let dhtRecord: DHTRecordDescriptor;
 | 
			
		||||
      const data = '🚀 This example DHT data with unicode a Ā 𐀀 文 🚀';
 | 
			
		||||
 | 
			
		||||
      before('create dht record', async () => {
 | 
			
		||||
        const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
        dhtRecord = await routingContext.createDhtRecord(
 | 
			
		||||
          {
 | 
			
		||||
            kind: 'DFLT',
 | 
			
		||||
            o_cnt: 1,
 | 
			
		||||
          },
 | 
			
		||||
          bestKind
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(dhtRecord.key).toBeDefined();
 | 
			
		||||
        expect(dhtRecord.owner).toBeDefined();
 | 
			
		||||
        expect(dhtRecord.owner_secret).toBeDefined();
 | 
			
		||||
        expect(dhtRecord.schema).toEqual({ kind: 'DFLT', o_cnt: 1 });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      after('free dht record', async () => {
 | 
			
		||||
        await routingContext.closeDhtRecord(dhtRecord.key);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should set value', async () => {
 | 
			
		||||
        const setValueRes = await routingContext.setDhtValue(
 | 
			
		||||
          dhtRecord.key,
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(data)
 | 
			
		||||
        );
 | 
			
		||||
        expect(setValueRes).toBeUndefined();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should get value with force refresh', async () => {
 | 
			
		||||
        const getValueRes = await routingContext.getDhtValue(
 | 
			
		||||
          dhtRecord.key,
 | 
			
		||||
          0,
 | 
			
		||||
          true
 | 
			
		||||
        );
 | 
			
		||||
        expect(getValueRes?.data).toBeDefined();
 | 
			
		||||
        expect(textDecoder.decode(getValueRes?.data)).toBe(data);
 | 
			
		||||
 | 
			
		||||
        expect(getValueRes?.writer).toBe(dhtRecord.owner);
 | 
			
		||||
        expect(getValueRes?.seq).toBe(0);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should open readonly record', async () => {
 | 
			
		||||
        await routingContext.closeDhtRecord(dhtRecord.key);
 | 
			
		||||
 | 
			
		||||
        const readonlyDhtRecord = await routingContext.openDhtRecord(
 | 
			
		||||
          dhtRecord.key
 | 
			
		||||
        );
 | 
			
		||||
        expect(readonlyDhtRecord).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        const setValueRes = routingContext.setDhtValue(
 | 
			
		||||
          dhtRecord.key,
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(data)
 | 
			
		||||
        );
 | 
			
		||||
        await expect(setValueRes).rejects.toEqual({
 | 
			
		||||
          kind: 'Generic',
 | 
			
		||||
          message: 'value is not writable',
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should open writable record', async () => {
 | 
			
		||||
        await routingContext.closeDhtRecord(dhtRecord.key);
 | 
			
		||||
 | 
			
		||||
        const writeableDhtRecord = await routingContext.openDhtRecord(
 | 
			
		||||
          dhtRecord.key,
 | 
			
		||||
          `${dhtRecord.owner}:${dhtRecord.owner_secret}`
 | 
			
		||||
        );
 | 
			
		||||
        expect(writeableDhtRecord).toBeDefined();
 | 
			
		||||
        const setValueRes = await routingContext.setDhtValue(
 | 
			
		||||
          dhtRecord.key,
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(`${data}👋`)
 | 
			
		||||
        );
 | 
			
		||||
        expect(setValueRes).toBeUndefined();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -6,7 +6,7 @@ import {
 | 
			
		||||
} from './utils/veilid-config';
 | 
			
		||||
 | 
			
		||||
import { VeilidTableDB, veilidClient } from 'veilid-wasm';
 | 
			
		||||
import { marshall, unmarshall } from './utils/marshalling-utils';
 | 
			
		||||
import { textEncoder, textDecoder } from './utils/marshalling-utils';
 | 
			
		||||
 | 
			
		||||
const TABLE_NAME = 'some-table';
 | 
			
		||||
const TABLE_COLS = 1;
 | 
			
		||||
@@ -57,18 +57,22 @@ describe('VeilidTable', () => {
 | 
			
		||||
      const value = 'test value with unicode 🚀';
 | 
			
		||||
 | 
			
		||||
      it('should store value', async () => {
 | 
			
		||||
        await table.store(0, marshall(key), marshall(value));
 | 
			
		||||
        await table.store(
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(key),
 | 
			
		||||
          textEncoder.encode(value)
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should load value', async () => {
 | 
			
		||||
        const storedValue = await table.load(0, marshall(key));
 | 
			
		||||
        const storedValue = await table.load(0, textEncoder.encode(key));
 | 
			
		||||
        expect(storedValue).toBeDefined();
 | 
			
		||||
        expect(unmarshall(storedValue!)).toBe(value);
 | 
			
		||||
        expect(textDecoder.decode(storedValue!)).toBe(value);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should have key in list of keys', async () => {
 | 
			
		||||
        const keys = await table.getKeys(0);
 | 
			
		||||
        const decodedKeys = keys.map(unmarshall);
 | 
			
		||||
        const decodedKeys = keys.map((key) => textDecoder.decode(key));
 | 
			
		||||
        expect(decodedKeys).toEqual([key]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
@@ -82,15 +86,27 @@ describe('VeilidTable', () => {
 | 
			
		||||
        const second = 'second✔';
 | 
			
		||||
        const third = 'third📢';
 | 
			
		||||
 | 
			
		||||
        transaction.store(0, marshall(key), marshall(first));
 | 
			
		||||
        transaction.store(0, marshall(key), marshall(second));
 | 
			
		||||
        transaction.store(0, marshall(key), marshall(third));
 | 
			
		||||
        transaction.store(
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(key),
 | 
			
		||||
          textEncoder.encode(first)
 | 
			
		||||
        );
 | 
			
		||||
        transaction.store(
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(key),
 | 
			
		||||
          textEncoder.encode(second)
 | 
			
		||||
        );
 | 
			
		||||
        transaction.store(
 | 
			
		||||
          0,
 | 
			
		||||
          textEncoder.encode(key),
 | 
			
		||||
          textEncoder.encode(third)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await transaction.commit();
 | 
			
		||||
 | 
			
		||||
        const storedValue = await table.load(0, marshall(key));
 | 
			
		||||
        const storedValue = await table.load(0, textEncoder.encode(key));
 | 
			
		||||
        expect(storedValue).toBeDefined();
 | 
			
		||||
        expect(unmarshall(storedValue!)).toBe(third);
 | 
			
		||||
        expect(textDecoder.decode(storedValue!)).toBe(third);
 | 
			
		||||
 | 
			
		||||
        transaction.free();
 | 
			
		||||
      });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,23 @@
 | 
			
		||||
//  TextEncoder/TextDecoder are used to solve for "The Unicode Problem" https://stackoverflow.com/a/30106551
 | 
			
		||||
export const textDecoder = new TextDecoder();
 | 
			
		||||
export const textEncoder = new TextEncoder();
 | 
			
		||||
 | 
			
		||||
export function marshall(data: string) {
 | 
			
		||||
  const byteString = bytesToString(new TextEncoder().encode(data));
 | 
			
		||||
//  TextEncoder/TextDecoder are used to solve for "The Unicode Problem" https://stackoverflow.com/a/30106551
 | 
			
		||||
export function marshallString(data: string) {
 | 
			
		||||
  return marshallBytes(textEncoder.encode(data));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function unmarshallString(b64: string) {
 | 
			
		||||
  return textDecoder.decode(unmarshallBytes(b64));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function marshallBytes(data: Uint8Array) {
 | 
			
		||||
  const byteString = bytesToString(data);
 | 
			
		||||
  return base64UrlEncode(byteString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function unmarshall(b64: string) {
 | 
			
		||||
export function unmarshallBytes(b64: string) {
 | 
			
		||||
  const byteString = base64UrlDecode(b64);
 | 
			
		||||
  return new TextDecoder().decode(stringToBytes(byteString));
 | 
			
		||||
  return stringToBytes(byteString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function base64UrlEncode(data: string) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import {
 | 
			
		||||
} from './utils/veilid-config';
 | 
			
		||||
 | 
			
		||||
import { veilidClient, veilidCrypto } from 'veilid-wasm';
 | 
			
		||||
import { textEncoder, unmarshallBytes } from './utils/marshalling-utils';
 | 
			
		||||
 | 
			
		||||
describe('veilidCrypto', () => {
 | 
			
		||||
  before('veilid startup', async () => {
 | 
			
		||||
@@ -29,10 +30,116 @@ describe('veilidCrypto', () => {
 | 
			
		||||
    expect(kinds.includes(bestKind)).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should generate key pair', async () => {
 | 
			
		||||
  it('should generate key pair', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
    const keypair = veilidCrypto.generateKeyPair(bestKind);
 | 
			
		||||
    expect(typeof keypair).toBe('string');
 | 
			
		||||
    // TODO: fix TypeScript return type of generateKeyPair to return string instead of KeyPair
 | 
			
		||||
 | 
			
		||||
    const [publicKey, secretKey] = keypair.split(':');
 | 
			
		||||
    expect(unmarshallBytes(publicKey).length).toBe(32);
 | 
			
		||||
    expect(unmarshallBytes(secretKey).length).toBe(32);
 | 
			
		||||
 | 
			
		||||
    const isValid = veilidCrypto.validateKeyPair(
 | 
			
		||||
      bestKind,
 | 
			
		||||
      publicKey,
 | 
			
		||||
      secretKey
 | 
			
		||||
    );
 | 
			
		||||
    expect(isValid).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should generate random bytes', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
    const bytes = veilidCrypto.randomBytes(bestKind, 64);
 | 
			
		||||
    expect(bytes instanceof Uint8Array).toBe(true);
 | 
			
		||||
    expect(bytes.length).toBe(64);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should hash data and validate hash', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
    const data = textEncoder.encode('this is my data🚀');
 | 
			
		||||
    const hash = veilidCrypto.generateHash(bestKind, data);
 | 
			
		||||
 | 
			
		||||
    expect(hash).toBeDefined();
 | 
			
		||||
    expect(typeof hash).toBe('string');
 | 
			
		||||
 | 
			
		||||
    const isValid = veilidCrypto.validateHash(bestKind, data, hash);
 | 
			
		||||
    expect(isValid).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should hash and validate password', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
 | 
			
		||||
    const password = textEncoder.encode('this is my data🚀');
 | 
			
		||||
    const saltLength = veilidCrypto.defaultSaltLength(bestKind);
 | 
			
		||||
    expect(saltLength).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
    const salt = veilidCrypto.randomBytes(bestKind, saltLength);
 | 
			
		||||
    expect(salt instanceof Uint8Array).toBe(true);
 | 
			
		||||
    expect(salt.length).toBe(saltLength);
 | 
			
		||||
 | 
			
		||||
    const hash = veilidCrypto.hashPassword(bestKind, password, salt);
 | 
			
		||||
    expect(hash).toBeDefined();
 | 
			
		||||
    expect(typeof hash).toBe('string');
 | 
			
		||||
 | 
			
		||||
    const isValid = veilidCrypto.verifyPassword(bestKind, password, hash);
 | 
			
		||||
    expect(isValid).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should aead encrypt and decrypt', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
    const body = textEncoder.encode(
 | 
			
		||||
      'This is an encoded body with my secret data in it🔥'
 | 
			
		||||
    );
 | 
			
		||||
    const ad = textEncoder.encode(
 | 
			
		||||
      'This is data associated with my secret data👋'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const nonce = veilidCrypto.randomNonce(bestKind);
 | 
			
		||||
    expect(typeof nonce).toBe('string');
 | 
			
		||||
 | 
			
		||||
    const sharedSecred = veilidCrypto.randomSharedSecret(bestKind);
 | 
			
		||||
    expect(typeof sharedSecred).toBe('string');
 | 
			
		||||
 | 
			
		||||
    const encBody = veilidCrypto.encryptAead(
 | 
			
		||||
      bestKind,
 | 
			
		||||
      body,
 | 
			
		||||
      nonce,
 | 
			
		||||
      sharedSecred,
 | 
			
		||||
      ad
 | 
			
		||||
    );
 | 
			
		||||
    expect(encBody instanceof Uint8Array).toBe(true);
 | 
			
		||||
 | 
			
		||||
    const overhead = veilidCrypto.aeadOverhead(bestKind);
 | 
			
		||||
    expect(encBody.length - body.length).toBe(overhead);
 | 
			
		||||
 | 
			
		||||
    const decBody = veilidCrypto.decryptAead(
 | 
			
		||||
      bestKind,
 | 
			
		||||
      encBody,
 | 
			
		||||
      nonce,
 | 
			
		||||
      sharedSecred,
 | 
			
		||||
      ad
 | 
			
		||||
    );
 | 
			
		||||
    expect(decBody instanceof Uint8Array).toBe(true);
 | 
			
		||||
    expect(body).toEqual(decBody);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should sign and verify', () => {
 | 
			
		||||
    const bestKind = veilidCrypto.bestCryptoKind();
 | 
			
		||||
    const keypair = veilidCrypto.generateKeyPair(bestKind);
 | 
			
		||||
    const data = textEncoder.encode(
 | 
			
		||||
      'This is some data I am signing with my key 🔑'
 | 
			
		||||
    );
 | 
			
		||||
    expect(typeof keypair).toBe('string');
 | 
			
		||||
 | 
			
		||||
    const [publicKey, secretKey] = keypair.split(':');
 | 
			
		||||
 | 
			
		||||
    const sig = veilidCrypto.sign(bestKind, publicKey, secretKey, data);
 | 
			
		||||
    expect(typeof sig).toBe('string');
 | 
			
		||||
 | 
			
		||||
    expect(() => {
 | 
			
		||||
      const res = veilidCrypto.verify(bestKind, publicKey, data, sig);
 | 
			
		||||
      expect(res).toBeUndefined();
 | 
			
		||||
    }).not.toThrow();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,20 @@ export const config: Options.Testrunner = {
 | 
			
		||||
  // Runner Configuration
 | 
			
		||||
  // ====================
 | 
			
		||||
  // WebdriverIO supports running e2e tests as well as unit and component tests.
 | 
			
		||||
  runner: ['browser', { viteConfig: './vite.config.ts' }],
 | 
			
		||||
  runner: [
 | 
			
		||||
    'browser',
 | 
			
		||||
    {
 | 
			
		||||
      viteConfig: './vite.config.ts',
 | 
			
		||||
      coverage: {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        // needed since the ../pkg directory that has the compiled wasm npm package
 | 
			
		||||
        //   is outside the current directory. Coverage is only collected on files
 | 
			
		||||
        //   that are in within `cwd`.
 | 
			
		||||
        cwd: '..',
 | 
			
		||||
        include: ['pkg/**'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  autoCompileOpts: {
 | 
			
		||||
    autoCompile: true,
 | 
			
		||||
    tsNodeOpts: {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user