0cee4e9ef31c

delete orphaned sync files: src/sync/, capacitor-sync-transport, sync-identity-glue, ml-detector, DiscoveredPeer type

time
2026-05-07 14:38 UTC · 5 months ago
machine
koh2
parent
18b8a770ad1e
6 files src/background/ml-detector.ts+0 −128src/background/sync-identity-glue.ts+0 −30src/shared/capacitor-sync-transport.ts+0 −226src/shared/sync-types.ts+0 −22src/sync/manifest.ts+0 −85src/sync/noise.ts+0 −192
src/background/ml-detector.ts+0 −128
@@ -1,128 +0,0 @@1-import * as ort from "onnxruntime-web";2-import type {3-  FieldPrediction,4-  FieldType,5-  MLFieldFeatures,6-} from "../shared/ml";7-8-const LABELS: FieldType[] = ["username", "password", "email", "totp", "none"];9-10-class BackgroundMLDetector {11-  private session: ort.InferenceSession | null = null;12-  private isLoading = false;13-  private loadPromise: Promise<void> | null = null;14-15-  async init(modelUrl?: string): Promise<void> {16-    if (this.session) return;17-    if (this.isLoading && this.loadPromise) {18-      await this.loadPromise;19-      return;20-    }21-22-    this.isLoading = true;23-    this.loadPromise = this.loadModel(modelUrl);24-    try {25-      await this.loadPromise;26-    } finally {27-      this.isLoading = false;28-    }29-  }30-31-  private async loadModel(modelUrl?: string): Promise<void> {32-    ort.env.wasm.wasmPaths = chrome.runtime.getURL("assets/");33-    ort.env.wasm.numThreads = 1;34-    ort.env.wasm.simd = true;35-36-    const url = modelUrl || chrome.runtime.getURL("models/form_detector.onnx");37-    this.session = await ort.InferenceSession.create(url);38-  }39-40-  async predict(features: MLFieldFeatures): Promise<FieldPrediction> {41-    if (!this.session) {42-      await this.init();43-    }44-    if (!this.session) {45-      throw new Error("ML model not initialized");46-    }47-48-    const vector = this.featuresToVector(features);49-    const input = new ort.Tensor("float32", vector, [1, 45]);50-    const inputName = this.session.inputNames[0];51-    const outputName = this.session.outputNames[0];52-    const results = await this.session.run({ [inputName]: input });53-    const probs = results[outputName].data as Float32Array;54-55-    let maxIndex = 0;56-    let maxProb = probs[0];57-    for (let i = 1; i < probs.length; i += 1) {58-      if (probs[i] > maxProb) {59-        maxProb = probs[i];60-        maxIndex = i;61-      }62-    }63-64-    return {65-      type: LABELS[maxIndex],66-      confidence: maxProb,67-      probabilities: {68-        username: probs[0],69-        password: probs[1],70-        email: probs[2],71-        totp: probs[3],72-        none: probs[4],73-      },74-    };75-  }76-77-  private featuresToVector(features: MLFieldFeatures): Float32Array {78-    return new Float32Array([79-      features.type_text,80-      features.type_email,81-      features.type_password,82-      features.type_tel,83-      features.type_number,84-      features.type_search,85-      features.type_url,86-      features.type_other,87-      features.auto_username,88-      features.auto_email,89-      features.auto_current_password,90-      features.auto_new_password,91-      features.auto_one_time_code,92-      features.auto_off,93-      features.auto_other,94-      features.name_has_user,95-      features.name_has_login,96-      features.name_has_email,97-      features.name_has_pass,98-      features.name_length,99-      features.id_has_user,100-      features.id_has_login,101-      features.id_has_email,102-      features.id_has_pass,103-      features.id_length,104-      features.placeholder_has_user,105-      features.placeholder_has_email,106-      features.placeholder_has_pass,107-      features.placeholder_length,108-      features.aria_label_has_user,109-      features.aria_label_has_email,110-      features.aria_label_has_pass,111-      features.aria_label_length,112-      features.parent_is_form,113-      features.parent_is_div,114-      features.parent_is_section,115-      features.sibling_count,116-      features.has_password_sibling,117-      features.has_email_sibling,118-      features.form_has_submit,119-      features.form_action_has_login,120-      features.is_required,121-      features.has_placeholder,122-      features.has_aria_label,123-      features.inputmode_numeric,124-    ]);125-  }126-}127-128-export const backgroundMLDetector = new BackgroundMLDetector();

view file at the parent

src/background/sync-identity-glue.ts+0 −30
@@ -1,30 +0,0 @@1-/**2- * Local device id helpers shared between the lifecycle tick and the3- * 6-digit pairing flow. Both need the same base64url-encoded 16-byte id4- * the native host expects in `Advertise` / `StartPairingServer`.5- */6-7-const DEVICE_ID_KEY = "peach_device_id";8-9-export async function getOrCreateLocalDeviceId(): Promise<string> {10-  try {11-    const stored = await chrome.storage.local.get(DEVICE_ID_KEY);12-    const value = (stored as Record<string, string | undefined>)[DEVICE_ID_KEY];13-    if (typeof value === "string" && value) return value;14-  } catch {15-    // fall through to generation16-  }17-  const random = crypto.getRandomValues(new Uint8Array(16));18-  let binary = "";19-  for (const byte of random) binary += String.fromCharCode(byte);20-  const id = btoa(binary)21-    .replace(/\+/g, "-")22-    .replace(/\//g, "_")23-    .replace(/=+$/g, "");24-  try {25-    await chrome.storage.local.set({ [DEVICE_ID_KEY]: id });26-  } catch {27-    // best-effort28-  }29-  return id;30-}

view file at the parent

src/shared/capacitor-sync-transport.ts+0 −226
@@ -1,226 +0,0 @@1-/**2- * Capacitor (Android) sync transport.3- *4- * The Android WebView cannot use the native messaging host (stdio is for5- * extension service workers only) and cannot open raw TCP sockets. The6- * desktop host exposes the same Noise NK protocol over WebSocket at7- * `tcp_port + 1` so this side speaks WebSocket binary frames carrying the8- * exact same bytes as the TCP path.9- *10- * Bytes-on-the-wire MUST stay identical to the native TCP/WS framing — see11- * `native/peach-sync-host/src/frame.rs`. A WebSocket Binary message carries12- * one logical Noise/auth/transport frame; the u32 length prefix used on TCP13- * is absent here because tungstenite already frames messages.14- */15-16-import { noiseNKInitiator, performInitiatorAuth, type NoiseNKSession } from "./noise-js";17-import type { SyncSessionTransport } from "../background/sync-coordinator";18-import type { DiscoveredPeer } from "./sync-types";19-20-const HANDSHAKE_TIMEOUT_MS = 10_000;21-22-function hexToBytes(hex: string): Uint8Array {23-  if (hex.length % 2 !== 0) throw new Error("invalid hex");24-  const out = new Uint8Array(hex.length / 2);25-  for (let i = 0; i < hex.length; i += 2) {26-    out[i / 2] = parseInt(hex.slice(i, i + 2), 16);27-  }28-  return out;29-}30-31-function base64urlEncode(bytes: Uint8Array): string {32-  let binary = "";33-  for (const byte of bytes) binary += String.fromCharCode(byte);34-  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");35-}36-37-function base64urlDecode(value: string): Uint8Array {38-  const padded = value39-    .replace(/-/g, "+")40-    .replace(/_/g, "/")41-    .padEnd(Math.ceil(value.length / 4) * 4, "=");42-  const binary = atob(padded);43-  const out = new Uint8Array(binary.length);44-  for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);45-  return out;46-}47-48-/**49- * Open a WebSocket to the peer, complete the Noise NK handshake + auth, and50- * return a SyncSessionTransport that runBlobSyncSession can drive.51- */52-export async function createCapacitorSyncTransport(53-  peer: DiscoveredPeer,54-  ourPrivateKey: Uint8Array,55-  ourPublicKey: Uint8Array,56-): Promise<SyncSessionTransport> {57-  if (peer.wsPort === undefined) {58-    throw new Error("peer did not advertise a WebSocket port");59-  }60-  const wsUrl = `ws://${peer.host}:${peer.wsPort}/`;61-62-  const ws = await new Promise<WebSocket>((resolve, reject) => {63-    const socket = new WebSocket(wsUrl);64-    socket.binaryType = "arraybuffer";65-    const timer = setTimeout(66-      () => {67-        try { socket.close(); } catch { /* best-effort */ }68-        reject(new Error("WebSocket open timeout"));69-      },70-      HANDSHAKE_TIMEOUT_MS,71-    );72-    socket.addEventListener("open", () => {73-      clearTimeout(timer);74-      resolve(socket);75-    });76-    socket.addEventListener("error", () => {77-      clearTimeout(timer);78-      reject(new Error(`Cannot connect to ${wsUrl}`));79-    });80-  });81-82-  // Single-message receive queue. The Noise handshake / auth round-trips83-  // alternate sends and receives in a strict order; any frame that arrives84-  // while no pending receiver exists is queued for the next caller.85-  const queue: Uint8Array[] = [];86-  let pending: ((data: Uint8Array) => void) | null = null;87-  let pendingReject: ((err: Error) => void) | null = null;88-  let closed = false;89-  let lastError: string | null = null;90-91-  ws.addEventListener("message", (event) => {92-    const data = new Uint8Array(event.data as ArrayBuffer);93-    if (pending) {94-      const resolve = pending;95-      pending = null;96-      pendingReject = null;97-      resolve(data);98-    } else {99-      queue.push(data);100-    }101-  });102-  ws.addEventListener("close", () => {103-    closed = true;104-    if (pendingReject) {105-      const reject = pendingReject;106-      pending = null;107-      pendingReject = null;108-      reject(new Error("WebSocket closed during handshake"));109-    }110-  });111-  ws.addEventListener("error", () => {112-    lastError = "websocket_error";113-    if (pendingReject) {114-      const reject = pendingReject;115-      pending = null;116-      pendingReject = null;117-      reject(new Error("WebSocket error"));118-    }119-  });120-121-  function rawSend(bytes: Uint8Array): Promise<void> {122-    if (closed) return Promise.reject(new Error("WebSocket closed"));123-    // Force a fresh ArrayBuffer copy so the WebSocket type checker doesn't124-    // hit the SharedArrayBuffer-or-ArrayBuffer ambiguity that some Uint8Array125-    // typings expose.126-    const copy = new Uint8Array(bytes);127-    ws.send(copy.buffer);128-    return Promise.resolve();129-  }130-131-  function rawReceive(): Promise<Uint8Array> {132-    if (queue.length > 0) return Promise.resolve(queue.shift()!);133-    if (closed) return Promise.reject(new Error("WebSocket closed"));134-    return new Promise<Uint8Array>((resolve, reject) => {135-      pending = resolve;136-      pendingReject = reject;137-    });138-  }139-140-  const responderPublicKey = hexToBytes(peer.x25519PublicKey);141-142-  // 1) Noise NK handshake bootstrap143-  const session: NoiseNKSession = await noiseNKInitiator(144-    responderPublicKey,145-    rawSend,146-    rawReceive,147-  );148-149-  // 2) Auth challenge/proof — encrypted via the post-handshake transport150-  const sendEncrypted = async (data: Uint8Array): Promise<void> => {151-    await rawSend(session.encrypt(data));152-  };153-  const receiveEncrypted = async (): Promise<Uint8Array> => {154-    return session.decrypt(await rawReceive());155-  };156-157-  await performInitiatorAuth(158-    session,159-    ourPrivateKey,160-    ourPublicKey,161-    responderPublicKey,162-    sendEncrypted,163-    receiveEncrypted,164-  );165-166-  // 3) Hand off to runBlobSyncSession via SyncSessionTransport.167-  // The session-coordinator framing is base64url-encoded JSON over Noise.168-  // We bridge it: outgoing frame string → bytes(base64url-decode) → encrypt169-  // → ws.send. Incoming ws.message → decrypt → bytes → base64url-encode →170-  // hand to onFrame handlers as a string.171-  const frameHandlers: Array<(frame: string) => void> = [];172-  const errorHandlers: Array<(message: string) => void> = [];173-  const closeHandlers: Array<() => void> = [];174-175-  // Replace the message listener with one that decrypts every frame and176-  // routes it to onFrame. Drain anything queued from the auth round.177-  ws.addEventListener("message", (event) => {178-    try {179-      const cipher = new Uint8Array(event.data as ArrayBuffer);180-      const plain = session.decrypt(cipher);181-      const frame = base64urlEncode(plain);182-      for (const h of frameHandlers) h(frame);183-    } catch (err) {184-      const msg = err instanceof Error ? err.message : String(err);185-      for (const h of errorHandlers) h(msg);186-    }187-  });188-  ws.addEventListener("close", () => {189-    for (const h of closeHandlers) h();190-  });191-  ws.addEventListener("error", () => {192-    const msg = lastError || "websocket_error";193-    for (const h of errorHandlers) h(msg);194-  });195-196-  return {197-    send: async (frame: string) => {198-      const plain = base64urlDecode(frame);199-      await rawSend(session.encrypt(plain));200-    },201-    onFrame: (handler) => {202-      frameHandlers.push(handler);203-      return () => {204-        const i = frameHandlers.indexOf(handler);205-        if (i >= 0) frameHandlers.splice(i, 1);206-      };207-    },208-    onError: (handler) => {209-      errorHandlers.push(handler);210-      return () => {211-        const i = errorHandlers.indexOf(handler);212-        if (i >= 0) errorHandlers.splice(i, 1);213-      };214-    },215-    onClose: (handler) => {216-      closeHandlers.push(handler);217-      return () => {218-        const i = closeHandlers.indexOf(handler);219-        if (i >= 0) closeHandlers.splice(i, 1);220-      };221-    },222-    close: () => {223-      try { ws.close(); } catch { /* best-effort */ }224-    },225-  };226-}

view file at the parent

src/shared/sync-types.ts+0 −22
@@ -105,25 +105,3 @@105105    */106106   connectionId?: string;107107 }108-109-/**110- * A peer the native sync host has seen on mDNS. The popup pairing flow lists111- * these and routes a tap to `INITIATE_PAIR_WITH_PEER` with the same shape.112- *113- * `alreadyPaired` is computed against `getPairedDevices()` at broadcast time114- * so the popup can render "Already paired ✓" without a second round-trip.115- */116-export interface DiscoveredPeer {117-  deviceId: string;118-  deviceName: string;119-  host: string;120-  port: number;121-  /**122-   * WebSocket port advertised by the peer (mDNS `wsport`). The Capacitor123-   * Android bridge connects through this; the desktop extension uses raw124-   * TCP via the native messaging host and ignores it.125-   */126-  wsPort?: number;127-  x25519PublicKey: string;128-  alreadyPaired: boolean;129-}

view file

src/sync/manifest.ts+0 −85
@@ -1,85 +0,0 @@1-import { VaultEntry } from "../shared/types";2-3-export interface ManifestEntry {4-  id: string;5-  modifiedAt: number;6-  deletedAt?: number;7-  hash: string;8-}9-10-export interface ManifestDiffResult {11-  sendToPeer: ManifestEntry[];12-  requestFromPeer: string[];13-  conflicts: ManifestEntry[];14-}15-16-const GC_TTL_MS = 90 * 24 * 60 * 60 * 1000;17-18-export function diffManifests(19-  localManifest: ManifestEntry[],20-  remoteManifest: ManifestEntry[]21-): ManifestDiffResult {22-  const localMap = new Map<string, ManifestEntry>(localManifest.map((e) => [e.id, e]));23-  const remoteMap = new Map<string, ManifestEntry>(remoteManifest.map((e) => [e.id, e]));24-25-  const sendToPeer: ManifestEntry[] = [];26-  const requestFromPeer: string[] = [];27-  const conflicts: ManifestEntry[] = [];28-29-  const allIds = new Set([...localMap.keys(), ...remoteMap.keys()]);30-31-  for (const id of allIds) {32-    const local = localMap.get(id);33-    const remote = remoteMap.get(id);34-35-    if (local && !remote) {36-      sendToPeer.push(local);37-    } else if (!local && remote) {38-      requestFromPeer.push(id);39-    } else if (local && remote) {40-      if (local.modifiedAt === remote.modifiedAt) {41-        if (local.hash !== remote.hash) {42-          conflicts.push(resolveConflict(local, remote));43-        }44-      } else if (local.modifiedAt > remote.modifiedAt) {45-        sendToPeer.push(local);46-      } else {47-        requestFromPeer.push(id);48-      }49-    }50-  }51-52-  return { sendToPeer, requestFromPeer, conflicts };53-}54-55-export function resolveConflict(a: ManifestEntry, b: ManifestEntry): ManifestEntry {56-  return a.id >= b.id ? a : b;57-}58-59-export function gcTombstones(entries: ManifestEntry[], nowMs: number): ManifestEntry[] {60-  return entries.filter(61-    (e) => !(e.deletedAt && e.deletedAt > 0 && nowMs - e.deletedAt > GC_TTL_MS)62-  );63-}64-65-export async function contentHash(entry: VaultEntry): Promise<string> {66-  const { id: _id, modifiedAt: _modifiedAt, deletedAt: _deletedAt, ...rest } = entry;67-  const json = JSON.stringify(rest);68-  const encoded = new TextEncoder().encode(json);69-  const digest = await crypto.subtle.digest("SHA-256", encoded);70-  const bytes = new Uint8Array(digest).slice(0, 16);71-  return Array.from(bytes)72-    .map((b) => b.toString(16).padStart(2, "0"))73-    .join("");74-}75-76-export async function buildManifest(entries: VaultEntry[]): Promise<ManifestEntry[]> {77-  return Promise.all(78-    entries.map(async (entry) => ({79-      id: entry.id,80-      modifiedAt: entry.modifiedAt,81-      deletedAt: entry.deletedAt,82-      hash: await contentHash(entry),83-    }))84-  );85-}

view file at the parent

src/sync/noise.ts+0 −192
@@ -1,192 +0,0 @@1-/**2- * Noise NK State Machine for Peach Sync3- *4- * Pattern: Noise NK5- * DH: X255196- * Cipher: ChaCha20-Poly13057- * Hash: BLAKE2s8- *9- * NK Pattern:10- *   <- s                    (pre-message: responder static public key)11- *   ...12- *   -> e, es                (initiator sends ephemeral key, performs DH with responder static)13- *   <- e, ee                (responder sends ephemeral key, performs DH)14- */15-16-import { x25519 } from "@noble/curves/ed25519.js";17-import { chacha20poly1305 } from "@noble/ciphers/chacha.js";18-import { blake2s } from "@noble/hashes/blake2.js";19-import { randomBytes } from "@noble/hashes/utils.js";20-21-export interface KeyPair {22-  publicKey: Uint8Array;23-  privateKey: Uint8Array;24-}25-26-export interface NoiseResult {27-  output: Uint8Array | null;28-  canSend: boolean;29-  isComplete: boolean;30-}31-32-export class NoiseNKStateMachine {33-  private staticKeyPair: KeyPair;34-  private ephemeralKeyPair: KeyPair | null = null;35-  private remoteEphemeralKey: Uint8Array | null = null;36-  private readonly isInitiator: boolean;37-  private remoteStaticKey: Uint8Array | null = null;38-  private sendNonce = 0;39-  private recvNonce = 0;40-  private sessionKey: Uint8Array | null = null;41-  private handshakeComplete = false;42-  private canSendMessages = false;43-44-  constructor(staticKeyPair: KeyPair, isInitiator: boolean) {45-    this.staticKeyPair = staticKeyPair;46-    this.isInitiator = isInitiator;47-  }48-49-  setRemoteStaticKey(key: Uint8Array): void {50-    if (this.isInitiator) {51-      this.remoteStaticKey = key;52-    }53-  }54-55-  private deriveSessionKey(dhOutputs: Uint8Array[]): Uint8Array {56-    const totalLength = dhOutputs.reduce((sum, arr) => sum + arr.length, 0);57-    const concat = new Uint8Array(totalLength);58-    let offset = 0;59-    for (const output of dhOutputs) {60-      concat.set(output, offset);61-      offset += output.length;62-    }63-    return blake2s(concat, { dkLen: 32 });64-  }65-66-  private performDH(localPrivate: Uint8Array, remotePublic: Uint8Array): Uint8Array {67-    return x25519.getSharedSecret(localPrivate, remotePublic);68-  }69-70-  private generateEphemeralKeyPair(): KeyPair {71-    const privateKey = randomBytes(32);72-    const publicKey = x25519.getPublicKey(privateKey);73-    return { publicKey, privateKey };74-  }75-76-  private encryptWithKey(key: Uint8Array, plaintext: Uint8Array, nonce: bigint): Uint8Array {77-    const nonceBytes = new Uint8Array(12);78-    const view = new DataView(nonceBytes.buffer);79-    view.setBigUint64(4, nonce, true);80-    const cipher = chacha20poly1305(key, nonceBytes);81-    return cipher.encrypt(plaintext);82-  }83-84-  private decryptWithKey(key: Uint8Array, ciphertext: Uint8Array, nonce: bigint): Uint8Array {85-    const nonceBytes = new Uint8Array(12);86-    const view = new DataView(nonceBytes.buffer);87-    view.setBigUint64(4, nonce, true);88-    const cipher = chacha20poly1305(key, nonceBytes);89-    return cipher.decrypt(ciphertext);90-  }91-92-  readMessage(data: Uint8Array): NoiseResult {93-    if (this.handshakeComplete) {94-      return { output: null, canSend: this.canSendMessages, isComplete: true };95-    }96-97-    if (this.isInitiator) {98-      if (!this.remoteStaticKey) {99-        throw new Error("Remote static key not set");100-      }101-102-      if (this.ephemeralKeyPair === null) {103-        this.ephemeralKeyPair = this.generateEphemeralKeyPair();104-        const e = this.ephemeralKeyPair.publicKey;105-        const es = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteStaticKey);106-        const key = this.deriveSessionKey([es]);107-        const output = this.encryptWithKey(key, data, BigInt(this.sendNonce));108-        this.sendNonce++;109-        return { output, canSend: true, isComplete: false };110-      } else {111-        this.remoteEphemeralKey = data;112-        const ee = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteEphemeralKey);113-        this.sessionKey = this.deriveSessionKey([ee]);114-        this.handshakeComplete = true;115-        this.canSendMessages = true;116-        return { output: null, canSend: true, isComplete: true };117-      }118-    } else {119-      if (this.remoteEphemeralKey === null) {120-        this.remoteEphemeralKey = data;121-        const e = this.staticKeyPair.publicKey;122-        const ee = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);123-        this.sessionKey = this.deriveSessionKey([ee]);124-        return { output: e, canSend: true, isComplete: false };125-      } else {126-        const es = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);127-        const key = this.deriveSessionKey([es]);128-        const output = this.decryptWithKey(key, data, BigInt(this.recvNonce));129-        this.recvNonce++;130-        this.sessionKey = this.deriveSessionKey([es, this.remoteEphemeralKey]);131-        this.handshakeComplete = true;132-        this.canSendMessages = true;133-        return { output, canSend: true, isComplete: true };134-      }135-    }136-  }137-138-  writeMessage(payload: Uint8Array): Uint8Array {139-    if (this.handshakeComplete) {140-      return this.encrypt(payload);141-    }142-143-    if (this.isInitiator) {144-      if (this.ephemeralKeyPair === null) {145-        this.ephemeralKeyPair = this.generateEphemeralKeyPair();146-        return this.ephemeralKeyPair.publicKey;147-      } else if (this.remoteEphemeralKey === null) {148-        if (!this.remoteStaticKey) {149-          throw new Error("Remote static key not set");150-        }151-        const es = this.performDH(this.ephemeralKeyPair.privateKey, this.remoteStaticKey);152-        const key = this.deriveSessionKey([es]);153-        return this.encryptWithKey(key, payload, BigInt(this.sendNonce));154-      }155-      throw new Error("Invalid handshake state");156-    } else {157-      if (this.remoteEphemeralKey === null) {158-        this.remoteEphemeralKey = payload;159-        return this.staticKeyPair.publicKey;160-      } else {161-        const es = this.performDH(this.staticKeyPair.privateKey, this.remoteEphemeralKey);162-        const key = this.deriveSessionKey([es]);163-        this.sessionKey = key;164-        this.handshakeComplete = true;165-        this.canSendMessages = true;166-        return this.encryptWithKey(key, payload, BigInt(this.sendNonce));167-      }168-    }169-  }170-171-  getSessionKey(): Uint8Array | null {172-    return this.sessionKey;173-  }174-175-  encrypt(plaintext: Uint8Array): Uint8Array {176-    if (!this.sessionKey) {177-      throw new Error("Session key not available");178-    }179-    const ciphertext = this.encryptWithKey(this.sessionKey, plaintext, BigInt(this.sendNonce));180-    this.sendNonce++;181-    return ciphertext;182-  }183-184-  decrypt(ciphertext: Uint8Array): Uint8Array {185-    if (!this.sessionKey) {186-      throw new Error("Session key not available");187-    }188-    const plaintext = this.decryptWithKey(this.sessionKey, ciphertext, BigInt(this.recvNonce));189-    this.recvNonce++;190-    return plaintext;191-  }192-}

view file at the parent