interface Dialect

A firmware family's wire protocol, expressed as pure functions and data.

Every member is pure: no I/O, no state, no time or randomness. Request builders return a Request that the orchestrator hands to fetch; parsers take the response text or headers. Because of that, every claim about a firmware's protocol is a string-in/string-out unit test needing no device.

Authoring a dialect

Dialects are authored by spreading an existing dialect and overriding only what differs. The interface deliberately has no optional members: optionality would force dialect.x ?? fallback at every call site, which is the pile of conditionals this design exists to avoid. Spread composition supplies the defaults instead.

Adding a member to this interface later stays source-compatible for spread-authored dialects, and is breaking only for dialects implemented from scratch — so spreading is the sanctioned style.

Examples

Author a dialect by spreading an existing one

import { assertEquals } from "@std/assert";
import type { Dialect } from "./dialect.ts";
import { gdprJson } from "./gdprJson.ts";

const vx800v: Dialect = {
  ...gdprJson,
  id: "vx800v",
  commandRequest: (baseUrl, envelope, session) =>
    new Request(new URL("cgi_gdpr", baseUrl), {
      method: "POST",
      headers: { TokenID: session.tokenId },
      body: `sign=${envelope.sign}\r\ndata=${envelope.data}\r\n`,
    }),
};

assertEquals(vx800v.id, "vx800v");
assertEquals(vx800v.defaultUsername, gdprJson.defaultUsername);

const request = vx800v.commandRequest(
  "http://192.168.1.1",
  { data: "ZGF0YQ==", sign: "00ff" },
  { sessionId: "sid", tokenId: "tok", authTimes: 1 },
);

assertEquals(request.url, "http://192.168.1.1/cgi_gdpr");
assertEquals(request.headers.get("TokenID"), "tok");

Properties

readonly
id: string

Stable identifier, carried on the authentication result for diagnostics.

readonly
defaultUsername: string

Username used when the caller supplies none.

Methods

infoRequest(baseUrl: string): Request

Builds the request for the login page, whose inline script carries authTimes and friends.

parseInfo(html: string): RouterInfo

Scrapes router variables out of the login page.

publicKeyRequest(baseUrl: string): Request

Builds the request for the RSA public key and sequence base.

parsePublicKey(text: string): PublicKeyInfo

Extracts RSA parameters from the public key response.

busyRequest(baseUrl: string): Request

Builds the request that reports whether a session is already established.

parseBusy(text: string): BusyStatus

Extracts login and busy flags from the busy response.

encodeLogin(credentials: Credentials): string

Encodes credentials into the plaintext login payload, pre-encryption.

loginRequest(
baseUrl: string,
envelope: Envelope
): Request

Builds the login request. Owns where the envelope rides — query string, request body, or anywhere else the firmware expects it.

parseSessionId(headers: Headers): string | null

Extracts the session id from the login response headers.

tokenRequest(
baseUrl: string,
session: Pick<SessionContext, "sessionId" | "authTimes">
): Request

Builds the request for the authenticated page carrying the security token.

parseTokenId(html: string): string

Extracts the security token from the authenticated page.

encodeCommands(actions: readonly Action[]): readonly CommandBatch[]

Splits actions into round trips and serializes each. Owns the operation vocabulary, stack defaults, and batching.

commandRequest(
baseUrl: string,
envelope: Envelope,
): Request

Builds a command request for one batch's envelope.

decodeCommand(
text: string,
): DecodedBatch

Decodes a decrypted command response into results aligned with the batch.