Files
suaveplan/packages/networking/drive/drive-webrtc/src/schemas/signaling.schemas.md
2026-05-27 23:47:53 +01:00

4.7 KiB

Signaling Schemas

Purpose

The signaling.schemas.ts module exposes Zod schemas mirroring every variant of SignalingMessage from types/messages.types.ts. Inbound traffic — both from the signaling WebSocket and from peer data channels — MUST be parsed against SignalingMessageSchema before any handler sees the value. The package treats anything that fails to parse as DRIVE_WEBRTC.SIGNALING_MESSAGE_INVALID, isolating untrusted input at the boundary so internal code can trust message shapes.

Schemas here are deliberately strict: required fields cannot be omitted, decimal-string u64 IDs must match /^\d+$/, and chunks arrays cannot contain empty strings. Permissive fields (from, to, chunkIds) use .optional() because the wire format allows them to be omitted.

Features

  • 17 per-variant schemas, one per SignalingMessage variant
  • SignalingMessageSchema z.union over all 17 — handles request-chunk shape overlap
  • Strict u64 decimal-string regex for fileId/chunkId
  • Strict RTCSessionDescriptionInit and RTCIceCandidateInit substructures
  • Non-empty string enforcement on requestId, peer IDs, chunk keys, base64 data
  • All schemas are pure validators — no transforms, no side effects

Usage

Basic Example

import { SignalingMessageSchema } from "@suaveplan/drive-webrtc/schemas";

const result = SignalingMessageSchema.safeParse(JSON.parse(rawWire));
if (!result.success) {
    // throw DriveWebRtcError(SIGNALING_MESSAGE_INVALID, ...)
}
const msg = result.data;

Advanced Example

import {
    ChunkDataPartMessageSchema,
    DRIVE_WEBRTC_CODES,
    DriveWebRtcError,
    SignalingMessageSchema,
} from "@suaveplan/drive-webrtc";

function parseFromDataChannel(raw: string): unknown {
    let value: unknown;
    try {
        value = JSON.parse(raw);
    } catch (cause) {
        throw new DriveWebRtcError(
            DRIVE_WEBRTC_CODES.SIGNALING_MESSAGE_INVALID,
            "not valid JSON",
            { cause: cause instanceof Error ? cause : undefined },
        );
    }

    // Sub-chunk fragments are hot-path — try the fast schema first
    const fast = ChunkDataPartMessageSchema.safeParse(value);
    if (fast.success) return fast.data;

    const slow = SignalingMessageSchema.safeParse(value);
    if (!slow.success) {
        throw new DriveWebRtcError(
            DRIVE_WEBRTC_CODES.SIGNALING_MESSAGE_INVALID,
            slow.error.message,
        );
    }
    return slow.data;
}

API Reference

Per-variant schemas (all exported):

  • OfferMessageSchema, AnswerMessageSchema, IceCandidateMessageSchema
  • RegisterMessageSchema, RegisteredMessageSchema
  • PeerJoinedMessageSchema, PeerLeftMessageSchema
  • AnnounceChunksMessageSchema, ChunksUpdatedMessageSchema
  • ChunkRequestMessageSchema, ChunkDataMessageSchema
  • ChunkDataPartMessageSchema
  • DataChannelChunkRequestSchema
  • ChunkQueryMessageSchema, ChunkQueryResponseSchema
  • ChunkBroadcastRequestSchema, ChunkBroadcastResponseSchema

SignalingMessageSchema

z.union over all 17 per-variant schemas. Use safeParse to validate untrusted input. Returns the typed SignalingMessage on success.

Implementation Notes

  • request-chunk is reused by both ChunkRequestMessage (signaling-relayed, nested payload object) and DataChannelChunkRequest (flat shape, no payload). z.discriminatedUnion requires unique discriminator values — we use z.union instead so Zod tries each schema in order until one matches.
  • The order of variants in the union matters for performance, not correctness: list hot-path variants first if profiling shows it. We currently match the typing-file order so audits are straightforward.
  • fileId and chunkId are validated as decimal-string u64s (/^\d+$/). The schema does NOT range-check against 2^64; the consumer should BigInt(...) and then reject any value above 2n ** 64n - 1n if that matters.
  • The RTCSessionDescriptionInit schema accepts the four standard type values (offer/answer/pranswer/rollback). The sdp field is optional because rollback legitimately omits it.
  • The RTCIceCandidateInit substructure permits null on sdpMLineIndex/sdpMid/usernameFragment because the browser DOM definitions allow it.
  • data fields (base64 payloads) are validated as non-empty strings only — full base64 character-set enforcement is deliberately omitted because some browsers tolerate / vs _ variants. Ciphertext integrity is verified separately after decode.
  • Adding a new message variant requires touching three files in one commit: types/messages.types.ts (the type), this file (the schema), and the union. The repo grep gate flags any drift between the three.