@mudlet/mudlet-web 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,9 +67,9 @@ export interface MudClientOptions {
67
67
  /** Whether the link to the *game server* is TLS-encrypted, reported as the
68
68
  * NEW-ENVIRON `TLS` capability. Defaults to whether `url` is `wss://` — the
69
69
  * correct answer for a direct websocket-mode connection. In proxy (`mud`)
70
- * mode the caller passes `false` explicitly, because a `wss://` proxy URL
71
- * only secures the browser↔proxy hop while the proxy↔MUD telnet socket is
72
- * plaintext (see connectionSecureTransport). */
70
+ * mode the caller passes the answer explicitly, because a `wss://` proxy URL
71
+ * only secures the browser↔proxy hop: the proxy↔MUD leg is plaintext telnet
72
+ * unless the profile enabled TLS (see connectionSecureTransport). */
73
73
  secureTransport?: boolean;
74
74
  /** Whether to advertise screen-reader use, reported as the MTTS SCREEN
75
75
  * READER bit and the NEW-ENVIRON `SCREEN_READER` capability
@@ -149,6 +149,14 @@ export declare class MudClient {
149
149
  * handshake this session, so a server that re-offers GMCP doesn't make us
150
150
  * announce ourselves twice. Reset on each connect(). */
151
151
  private gmcpHelloSent;
152
+ /** Set when this dial asked the proxy for TLS (`&tls=1` in the URL), so the
153
+ * client knows to expect a `tls.established` control frame and to police
154
+ * the deadline below. */
155
+ private readonly tlsRequested;
156
+ /** True once the proxy confirmed the handshake, or once any game bytes have
157
+ * arrived (which can only happen through a working tunnel). */
158
+ private tlsResolved;
159
+ private tlsDeadline;
152
160
  commandEcho: boolean;
153
161
  /** Gates the in-band `!!SOUND(...)` / `!!MUSIC(...)` tag parsing — the tag
154
162
  * bytes are legitimate text on non-MSP MUDs. */
@@ -176,6 +184,11 @@ export declare class MudClient {
176
184
  setPromptTimeoutMs(ms: number): void;
177
185
  getPromptTimeoutMs(): number;
178
186
  connect(): void;
187
+ /** Stop policing the TLS deadline — the handshake is accounted for. */
188
+ private resolveTls;
189
+ /** Decode one proxy control frame. Anything unrecognised is ignored so a
190
+ * newer proxy can add message types without breaking older clients. */
191
+ private handleControlFrame;
179
192
  disconnect(): void;
180
193
  /** Whether MSP is live on this connection (negotiated, not merely allowed
181
194
  * by the profile config) — gates Mudlet's receiveMSP. */
@@ -2,6 +2,55 @@ import type { AnsiAwareBuffer } from './text/FormatState';
2
2
  import type { MspCommand } from './protocol';
3
3
  import type { ScriptLogSource } from './MudSession';
4
4
  export type SessionStatus = 'disconnected' | 'connecting' | 'connected';
5
+ /** The peer certificate as reported by the proxy. Mirrors the four fields
6
+ * Mudlet shows (issuer / issued-to / expiry / serial) plus a little extra for
7
+ * diagnostics. All strings, already formatted for display. */
8
+ export interface TlsCertInfo {
9
+ subject: string;
10
+ subjectOrg: string;
11
+ issuer: string;
12
+ issuerOrg: string;
13
+ validFrom: string;
14
+ validTo: string;
15
+ serial: string;
16
+ fingerprint: string;
17
+ altNames: string;
18
+ }
19
+ export interface TlsEstablished {
20
+ /** Negotiated protocol, e.g. `TLSv1.3`. Empty when the proxy can't report it. */
21
+ protocol: string;
22
+ /** Negotiated cipher suite name. Empty when the proxy can't report it. */
23
+ cipher: string;
24
+ cert: TlsCertInfo | null;
25
+ /** False when the proxy runtime cannot inspect certificates at all. */
26
+ certInspection: boolean;
27
+ /** Certificate faults tolerated because of the profile's ignore-flags. */
28
+ acceptedDespite: string[];
29
+ /** Cert-tolerance options this proxy was asked for but cannot honour. */
30
+ unsupportedOptions: string[];
31
+ }
32
+ /** What the UI knows about the current connection's TLS state. */
33
+ export type TlsStatus = {
34
+ kind: 'established';
35
+ info: TlsEstablished;
36
+ } | {
37
+ kind: 'error';
38
+ info: TlsError;
39
+ }
40
+ /** TLS was asked for but nothing came back — see the `tls.timeout` event. */
41
+ | {
42
+ kind: 'timeout';
43
+ host: string;
44
+ port: number;
45
+ };
46
+ export interface TlsError {
47
+ /** Primary blocking fault, e.g. `CERT_HAS_EXPIRED`. */
48
+ code: string;
49
+ message: string;
50
+ codes: string[];
51
+ cert: TlsCertInfo | null;
52
+ certInspection: boolean;
53
+ }
5
54
  /** A pending Mudlet `invokeFileDialog(...)` request. The Lua handler that
6
55
  * called it is suspended (parked coroutine) until `onPick` fires, so the UI
7
56
  * must always resolve it eventually — pass the picked VFS path, or '' for
@@ -26,6 +75,24 @@ export type MudClientEvents = {
26
75
  /** The WebSocket subprotocol the server selected from our advertised list
27
76
  * (RFC 6455), or '' if none — only emitted when we advertised any. */
28
77
  'client.subprotocol': [protocol: string];
78
+ /** The proxy completed a TLS handshake with the game and the link is
79
+ * carrying decrypted traffic. `cert` is null when the proxy cannot inspect
80
+ * certificates (the Cloudflare Worker runtime can't), in which case
81
+ * `certInspection` is false. `acceptedDespite` lists any certificate faults
82
+ * the profile's ignore-flags waved through — non-empty means encrypted but
83
+ * not authenticated. */
84
+ 'tls.established': [info: TlsEstablished];
85
+ /** The proxy refused the game's certificate, or the TLS handshake failed.
86
+ * The connection is closing; `codes` carries every blocking fault. */
87
+ 'tls.error': [info: TlsError];
88
+ /** TLS was requested but the link produced no evidence of a handshake before
89
+ * the deadline. Distinct from `tls.error` because the cause is ambiguous:
90
+ * a proxy too old to understand `&tls=1`, or a Cloudflare-Worker-backed
91
+ * proxy where a rejected certificate hangs silently instead of reporting. */
92
+ 'tls.timeout': [info: {
93
+ host: string;
94
+ port: number;
95
+ }];
29
96
  'gmcp.negotiated': void;
30
97
  'msdp.negotiated': void;
31
98
  'mssp.negotiated': void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * MSSP-advertised TLS port tracking.
3
+ *
4
+ * Mirrors Mudlet's `cTelnet::promptTlsConnectionAvailable` (ctelnet.cpp) —
5
+ * a server may advertise a secure port through the MSSP `TLS` / `SSL` variable,
6
+ * and the client offers, once, to switch to it.
7
+ *
8
+ * Kept free of React and session plumbing so the decision rules can be tested
9
+ * directly; the caller supplies the current connection state.
10
+ */
11
+ /** Per-connection MSSP facts that bear on the TLS offer. */
12
+ export interface MsspTlsFacts {
13
+ /** The advertised secure port, or 0 when none/unusable. */
14
+ tlsPort: number;
15
+ /** MSSP `HOSTNAME`, used to catch an advertisement meant for a different host. */
16
+ hostName: string;
17
+ }
18
+ export declare function emptyMsspTlsFacts(): MsspTlsFacts;
19
+ /**
20
+ * Fold one MSSP variable into the accumulated facts.
21
+ *
22
+ * `TLS`/`SSL` normally carries a port number, but the values `-1` and `1` are
23
+ * used by some servers as plain "unsupported"/"supported" booleans. Mudlet
24
+ * discards both rather than dialling port 1, and so do we.
25
+ */
26
+ export declare function applyMsspVariable(facts: MsspTlsFacts, name: string, value: string): MsspTlsFacts;
27
+ /** True for a literal IPv4/IPv6 address. A certificate is very unlikely to be
28
+ * issued for a bare IP, so Mudlet suppresses the offer in that case rather
29
+ * than steering the user into a guaranteed validation failure. */
30
+ export declare function isIpAddress(host: string): boolean;
31
+ export interface TlsOfferState {
32
+ /** MSSP facts gathered on this connection. */
33
+ facts: MsspTlsFacts;
34
+ /** The host this profile actually dialled. */
35
+ host: string;
36
+ /** The port currently in use. */
37
+ port: number;
38
+ /** Whether this profile is already connecting over TLS. */
39
+ tlsEnabled: boolean;
40
+ /** The profile's "remind me about secure connections" preference. */
41
+ askTlsAvailable: boolean;
42
+ /** True while an offer is already on screen, so repeats can't stack. */
43
+ promptInFlight: boolean;
44
+ /** Only proxy-mode connections can be upgraded: an MSSP TLS port is a raw
45
+ * telnet-over-TLS port, which a browser cannot dial directly. */
46
+ proxyMode: boolean;
47
+ }
48
+ /**
49
+ * Decide whether to offer the secure-port switch. Mirrors Mudlet's guard set:
50
+ * a usable advertised port, not already encrypted, the reminder still enabled,
51
+ * the host not a bare IP, MSSP `HOSTNAME` (when given) matching the host we
52
+ * dialled, and no offer already pending.
53
+ */
54
+ export declare function shouldOfferTlsUpgrade(s: TlsOfferState): boolean;
@@ -0,0 +1,4 @@
1
+ export declare function describeCertCode(code: string): string;
2
+ /** Which of Mudlet's three tolerance checkboxes would clear a given fault.
3
+ * `all` means only "accept all certificate errors" covers it. */
4
+ export declare function toleranceForCode(code: string): 'expired' | 'selfSigned' | 'all';
@@ -66,6 +66,12 @@ export interface IScriptingRuntime {
66
66
  * denyCurrentSend().
67
67
  */
68
68
  dispatchSendRequest(text: string): boolean;
69
+ /**
70
+ * Start a speedwalk between two rooms (Mudlet Host::startSpeedWalk): find
71
+ * the path and hand it to the mapper package's `doSpeedWalk`. Driven by the
72
+ * map's double-click-to-walk gesture via WindowManager.startSpeedWalk.
73
+ */
74
+ startSpeedWalk(from: number, to: number): void;
69
75
  /**
70
76
  * Kill every event handler registered by `wrapScript` for the given
71
77
  * script id. Called when a script is removed or disabled so its handlers
@@ -630,6 +630,14 @@ export declare class ScriptingAPI {
630
630
  * decides the behaviour (send / prompt / open URL); anything else is a
631
631
  * no-op (it was already rejected at parse time). */
632
632
  private runHyperlinkUri;
633
+ /**
634
+ * Activate an `<a href>` clicked inside a label's rich text — Mudlet's
635
+ * `TLabel::slot_linkActivated`. Wired onto the LabelManager in the
636
+ * constructor; see labelLinks.ts for how the schemes differ from the OSC 8
637
+ * ones {@link runHyperlinkUri} handles (chiefly: a scheme-less href is a Lua
638
+ * chunk, which is how Geyser packages hang code off a label link).
639
+ */
640
+ activateLabelLink(href: string): void;
633
641
  /**
634
642
  * Build a {@link FormatHyperlink} for an OSC 8 link URI. The scheme decides
635
643
  * the behaviour, mirroring Mudlet: `send:` fires the command immediately,
@@ -696,7 +696,7 @@ export declare class ScriptingEngine implements EngineHost {
696
696
  private performReset;
697
697
  /**
698
698
  * Resolves once the initial load pass has finished (scripts/aliases/timers/
699
- * keys applied, sysLoadEvent fired, triggers compiled). The auto-connect
699
+ * keys applied, triggers compiled, sysLoadEvent fired). The auto-connect
700
700
  * path awaits this so the MUD socket isn't dialed until every handler and
701
701
  * trigger is in place. Always resolves — never rejects — so a failed or
702
702
  * torn-down load can't leave a profile waiting to connect forever.
@@ -103,6 +103,14 @@ export declare class LuaRuntime implements IScriptingRuntime {
103
103
  private dispatchCb;
104
104
  private dispatchCbWithArg;
105
105
  private unregisterCb;
106
+ /**
107
+ * Mudlet `T2DMap::initiateSpeedWalk` / `Host::startSpeedWalk` — the map's
108
+ * double-click-to-walk gesture. Pathfinds `from` → `to` (unless the mapper
109
+ * opted into `mudlet.custom_speedwalk`) and calls the mapper package's
110
+ * `doSpeedWalk`; see `__mudix_start_speedwalk` in Bridge.lua. Errors inside
111
+ * the mapper are reported, never thrown at the UI caller.
112
+ */
113
+ startSpeedWalk(from: number, to: number): void;
106
114
  private execModule;
107
115
  private dispatchingEvent;
108
116
  private readonly pendingEvents;
@@ -1,4 +1,4 @@
1
- export { type AppSchema, type MudConnection, type ConnectionMode, type ClientSettings, type ProfileSettings, type MapperSettings, type MapInfoBgColor, type ProtocolSettings, type BooleanProtocolKey, type Theme, type OutputFontSource, APP_DEFAULTS, PROFILE_DEFAULTS, MAPPER_DEFAULTS, MAP_INFO_BG_DEFAULT, PROTOCOL_DEFAULTS, WS_SUBPROTOCOL_CHOICES, DEFAULT_PROXY_URL, connectionUrl, connectionDisplayAddr, connectionSecureTransport, selectProfileField } from './schema';
1
+ export { type AppSchema, type MudConnection, type ConnectionMode, type ClientSettings, type ProfileSettings, type MapperSettings, type MapInfoBgColor, type ProtocolSettings, type BooleanProtocolKey, type Theme, type OutputFontSource, APP_DEFAULTS, PROFILE_DEFAULTS, MAPPER_DEFAULTS, MAP_INFO_BG_DEFAULT, PROTOCOL_DEFAULTS, WS_SUBPROTOCOL_CHOICES, DEFAULT_PROXY_URL, connectionUrl, connectionDisplayAddr, connectionSecureTransport, effectiveProxyUrl, proxyCanInspectCertificates, selectProfileField } from './schema';
2
2
  export { useAppStore } from './appStore';
3
3
  export { initCrossTabSync } from './crossTabSync';
4
4
  export { ConnectionIdContext, useConnectionId, useProfileField, useClientField, useEffectiveTheme } from './hooks';
@@ -44,6 +44,28 @@ export interface MudConnection {
44
44
  * already ship them — Mudlet installs those only into brand-new profiles, so
45
45
  * their absence in an imported profile is a real choice, not a gap to fill. */
46
46
  mudletImported?: boolean;
47
+ /** Connect to the game over TLS — Mudlet's `mSslTsl` / the connection
48
+ * dialog's "secure connection" checkbox. Meaningful in `mud` (proxy) mode
49
+ * only: the browser cannot wrap a raw socket itself, so the proxy performs
50
+ * the handshake on our behalf (`&tls=1`). In `websocket` mode the URL
51
+ * scheme already decides it, so this flag is ignored there. */
52
+ tls?: boolean;
53
+ /** Tolerate an expired peer certificate (Mudlet's `mSslIgnoreExpired`). */
54
+ sslIgnoreExpired?: boolean;
55
+ /** Tolerate a self-signed peer certificate (Mudlet's `mSslIgnoreSelfSigned`). */
56
+ sslIgnoreSelfSigned?: boolean;
57
+ /** Tolerate *every* certificate fault, hostname mismatch included — this
58
+ * gives up the authentication half of TLS and leaves only encryption
59
+ * (Mudlet's `mSslIgnoreAll`, labelled "unsecure" in its UI).
60
+ *
61
+ * ⚠ All three are honoured only by the Node proxy. The Cloudflare Worker
62
+ * runtime's `connect()` exposes no way to inspect or override certificate
63
+ * validation, so a worker-backed profile silently ignores them. */
64
+ sslIgnoreAll?: boolean;
65
+ /** The plaintext port in use before an MSSP-advertised TLS upgrade, kept so
66
+ * a failed upgrade can be reverted in one click. Cleared once a secure
67
+ * connection has actually worked. */
68
+ preTlsPort?: number;
47
69
  /** Free-text profile description (Mudlet's profile "description" field, read/
48
70
  * written by getProfileInformation / setProfileInformation /
49
71
  * clearProfileInformation). Lives on the connection record — not the VFS-
@@ -105,6 +127,12 @@ export interface ProfileSettings {
105
127
  * installed automatically). Disable to ignore those requests. Per-profile
106
128
  * so each MUD is trusted independently. */
107
129
  allowMudPackageInstall?: boolean;
130
+ /** When true (default), a server that advertises a TLS port via MSSP prompts
131
+ * once to switch to it. Declining sets this false so the offer never
132
+ * reappears for this profile. Mudlet calls it `mAskTlsAvailable`
133
+ * ("Allow secure connection reminder"), settable from Lua via
134
+ * `setProfileConfig("askTlsAvailable", …)`. */
135
+ askTlsAvailable?: boolean;
108
136
  showTimestamps: boolean;
109
137
  fontSize: number;
110
138
  outputBackground: string;
@@ -626,13 +654,31 @@ export declare const APP_DEFAULTS: AppSchema;
626
654
  */
627
655
  export declare function selectProfileField<K extends keyof ProfileSettings>(s: Pick<AppSchema, 'connectionProfile'>, connectionId: string | null, key: K): ProfileSettings[K];
628
656
  export declare function connectionUrl(c: MudConnection, userProxyUrl?: string): string;
657
+ /** The proxy a `mud`-mode connection will actually dial, applying the same
658
+ * precedence as {@link connectionUrl}: per-connection > user's own > brand >
659
+ * built-in default. */
660
+ export declare function effectiveProxyUrl(c: MudConnection, userProxyUrl?: string): string;
661
+ /**
662
+ * Whether a proxy can inspect the game's certificate — and therefore whether
663
+ * the "accept expired / self-signed / all" options mean anything.
664
+ *
665
+ * Only the Node proxy can: it uses `tls.connect` and reads the peer certificate.
666
+ * A Cloudflare Worker cannot, because `cloudflare:sockets` `connect()` exposes
667
+ * no certificate and no way to waive a validation failure — the options would be
668
+ * silently ignored, so the UI disables them instead of pretending.
669
+ *
670
+ * Recognised by the `workers.dev` hostname, which covers the built-in default
671
+ * proxy and anything deployed from `worker/`. A Worker on a custom domain can't
672
+ * be told apart from a Node proxy up front; that case is corrected at runtime by
673
+ * the `certInspection: false` flag the proxy reports on `tls.established`.
674
+ */
675
+ export declare function proxyCanInspectCertificates(proxyUrl: string): boolean;
629
676
  export declare function connectionDisplayAddr(c: MudConnection): string;
630
677
  /** Whether the connection's link to the *game server* is TLS-encrypted — the
631
678
  * signal reported as the NEW-ENVIRON `TLS` variable. In `websocket` mode the
632
679
  * browser connects straight to the game, so a `wss://` URL is end-to-end TLS.
633
- * In `mud` (proxy) mode the browser↔proxy hop may be `wss://`, but the proxy
634
- * reaches the MUD over a raw TCP telnet socket (`net.connect`, no upstream
635
- * TLS) so the server's inbound connection is always plaintext and this is
636
- * false regardless of the proxy URL scheme. */
680
+ * In `mud` (proxy) mode the browser↔proxy hop being `wss://` says nothing about
681
+ * the proxy↔game hop: that leg is a plaintext telnet socket unless `tls` is set,
682
+ * which makes the proxy perform a TLS handshake with the game instead. */
637
683
  export declare function connectionSecureTransport(c: MudConnection): boolean;
638
684
  export {};