@vincentt-xr/cli 0.19.0 → 0.21.0

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.
Files changed (67) hide show
  1. package/MIGRATION.md +25 -0
  2. package/README.md +3 -2
  3. package/dist/cli/index.js +95 -13
  4. package/dist/cli/usage-text.js +3 -2
  5. package/dist/domain/api.js +16 -12
  6. package/dist/domain/command.js +9 -0
  7. package/dist/dx/command.js +3 -0
  8. package/dist/dx/consent.js +10 -14
  9. package/dist/dx/reportIo.js +6 -4
  10. package/dist/dx/send.js +6 -4
  11. package/dist/lib/backend.js +5 -10
  12. package/dist/lib/exitCodes.d.ts +6 -0
  13. package/dist/lib/exitCodes.js +6 -0
  14. package/dist/lib/terminalText.d.ts +6 -0
  15. package/dist/lib/terminalText.js +10 -0
  16. package/dist/lib/whoami.js +3 -0
  17. package/dist/library/assetsCommand.js +4 -0
  18. package/dist/library/client.js +6 -8
  19. package/dist/login/authBackend.d.ts +27 -2
  20. package/dist/login/authBackend.js +86 -17
  21. package/dist/login/loginCommand.d.ts +35 -3
  22. package/dist/login/loginCommand.js +81 -5
  23. package/dist/login/machineLabel.d.ts +46 -0
  24. package/dist/login/machineLabel.js +119 -0
  25. package/dist/login/pkceLogin.d.ts +17 -1
  26. package/dist/login/pkceLogin.js +30 -5
  27. package/dist/platform/releaseFetch.js +36 -69
  28. package/dist/preview/api.js +9 -12
  29. package/dist/preview/command.d.ts +3 -2
  30. package/dist/preview/command.js +8 -5
  31. package/dist/preview/copy.d.ts +8 -1
  32. package/dist/preview/copy.js +17 -4
  33. package/dist/preview/proxy.js +5 -39
  34. package/dist/preview/runner.d.ts +25 -9
  35. package/dist/preview/runner.js +42 -15
  36. package/dist/publish/api.js +14 -14
  37. package/dist/publish/command.d.ts +3 -0
  38. package/dist/publish/command.js +71 -7
  39. package/dist/publish/copy.d.ts +9 -0
  40. package/dist/publish/copy.js +83 -14
  41. package/dist/publish/keyScan.d.ts +25 -0
  42. package/dist/publish/keyScan.js +88 -0
  43. package/dist/publish/manifest.d.ts +51 -19
  44. package/dist/publish/manifest.js +246 -63
  45. package/dist/publish/upload.d.ts +8 -0
  46. package/dist/publish/upload.js +15 -0
  47. package/dist/publish/weight.d.ts +23 -0
  48. package/dist/publish/weight.js +113 -0
  49. package/dist/shared/authError.d.ts +1 -1
  50. package/dist/shared/authError.js +3 -1
  51. package/dist/shared/credentialNames.d.ts +17 -0
  52. package/dist/shared/credentialNames.js +72 -0
  53. package/dist/shared/http.d.ts +34 -0
  54. package/dist/shared/http.js +98 -2
  55. package/dist/shared/upgradeError.d.ts +62 -0
  56. package/dist/shared/upgradeError.js +215 -0
  57. package/dist/tunnel/caps.d.ts +19 -0
  58. package/dist/tunnel/caps.js +52 -0
  59. package/dist/tunnel/cbor.d.ts +39 -12
  60. package/dist/tunnel/cbor.js +84 -10
  61. package/dist/tunnel/client.d.ts +48 -2
  62. package/dist/tunnel/client.js +157 -44
  63. package/dist/tunnel/frame.d.ts +1 -1
  64. package/dist/tunnel/frame.js +8 -4
  65. package/dist/tunnel/index.d.ts +3 -2
  66. package/dist/tunnel/index.js +3 -2
  67. package/package.json +4 -3
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Capability negotiation, the client's half (f3 §2.2.2). The client offers on
3
+ * every dial; the edge answers the agreed set on the 101, or omits the header
4
+ * when it agreed nothing. The agreed set fixes this connection's decode grammar.
5
+ */
6
+ import { BASE_CAPS, TUNNEL_CAPS } from "./cbor.js";
7
+ /** Lower-case: Node lower-cases response header names. */
8
+ export const CAPS_HEADER = "x-vincentt-tunnel-caps";
9
+ /** Every element this client understands, offered on every dial. */
10
+ export const OFFERED_CAPS = TUNNEL_CAPS;
11
+ export const OFFERED_CAPS_VALUE = OFFERED_CAPS.join(", ");
12
+ const TOKEN_RE = /^[a-z][a-z0-9-]{0,31}$/;
13
+ const MAX_TOKENS = 8;
14
+ const MAX_BYTES = 256;
15
+ /**
16
+ * The agreed set from a 101, or null for a protocol violation: the header
17
+ * duplicated, an array, ungrammatical, or naming a token this client did not
18
+ * offer. Absent is the base protocol, never a violation. `rawHeaders` is
19
+ * consulted because Node joins a duplicated header into one comma list, which
20
+ * would otherwise read as a grammatical value.
21
+ */
22
+ export function parseAgreedCaps(value, rawHeaders, offered = OFFERED_CAPS) {
23
+ if (rawHeaders !== undefined) {
24
+ let seen = 0;
25
+ for (let i = 0; i < rawHeaders.length; i += 2) {
26
+ if (rawHeaders[i]?.toLowerCase() === CAPS_HEADER)
27
+ seen++;
28
+ }
29
+ if (seen > 1)
30
+ return null;
31
+ }
32
+ if (value === undefined)
33
+ return BASE_CAPS;
34
+ if (typeof value !== "string")
35
+ return null;
36
+ if (Buffer.byteLength(value, "utf8") > MAX_BYTES)
37
+ return null;
38
+ const tokens = value.split(",").map((t) => t.replace(/^ +| +$/g, ""));
39
+ if (tokens.length > MAX_TOKENS)
40
+ return null;
41
+ const agreed = new Set();
42
+ for (const t of tokens) {
43
+ if (!TOKEN_RE.test(t))
44
+ return null;
45
+ if (!offered.includes(t))
46
+ return null;
47
+ if (agreed.has(t))
48
+ return null;
49
+ agreed.add(t);
50
+ }
51
+ return agreed;
52
+ }
@@ -15,6 +15,15 @@
15
15
  * themselves, but because the encoder we own never emits them, so accepting
16
16
  * them would only widen the input this parser has to be correct about.
17
17
  */
18
+ /**
19
+ * The optional protocol elements a connection may agree on the upgrade (f3
20
+ * §2.2.2). Each changes one disjoint grammar element.
21
+ */
22
+ export declare const TUNNEL_CAPS: readonly ["open-body", "control-authz-expired"];
23
+ export type TunnelCap = (typeof TUNNEL_CAPS)[number];
24
+ export type CapSet = ReadonlySet<TunnelCap>;
25
+ /** The agreed-nothing grammar: exactly the pre-negotiation protocol. */
26
+ export declare const BASE_CAPS: CapSet;
18
27
  /**
19
28
  * VIEWER_LABEL_RE is the SAME regex the edge assigns against, and this client
20
29
  * RE-VALIDATES rather than trusting the edge. That is closure (c) of the three
@@ -39,6 +48,11 @@ export interface OpenPayload {
39
48
  method: string;
40
49
  path: string;
41
50
  headers: [string, string][];
51
+ /**
52
+ * Whether a request body follows. Present on every non-upgrade OPEN iff the
53
+ * connection agreed `open-body`; never present with `upgrade`.
54
+ */
55
+ body?: boolean;
42
56
  upgrade?: string;
43
57
  }
44
58
  export interface HeadPayload {
@@ -46,26 +60,39 @@ export interface HeadPayload {
46
60
  headers: [string, string][];
47
61
  }
48
62
  /**
49
- * `viewer_gone` is the ONE non-terminal kind: the other three mean the session
50
- * is ending, this one means a viewer's connection closed while the session
51
- * continues. It is also the only kind carrying a second key.
63
+ * A closed enum of five. `viewer_gone` is the ONE non-terminal kind: the others
64
+ * mean the session is ending, this one means a viewer's connection closed while
65
+ * the session continues. It is also the only kind carrying a second key.
66
+ * `authz_expired` exists only on a connection that agreed
67
+ * `control-authz-expired`.
52
68
  */
53
- export type ControlKind = "revoke" | "expiring" | "superseded" | "viewer_gone";
69
+ export type ControlKind = "revoke" | "expiring" | "superseded" | "authz_expired" | "viewer_gone";
54
70
  /** A decoded CONTROL payload. `viewerLabel` is present only on `viewer_gone`. */
55
71
  export interface ControlPayload {
56
72
  kind: ControlKind;
57
73
  viewerLabel?: string;
58
74
  }
59
- export declare function decodeOpen(b: Uint8Array): OpenPayload;
75
+ /**
76
+ * Decode an OPEN under the connection's agreed set. `body` on a connection that
77
+ * did not agree `open-body`, its absence on a non-upgrade OPEN that did, and
78
+ * `body` beside `upgrade` are all fatal: only the edge writes that key.
79
+ */
80
+ export declare function decodeOpen(b: Uint8Array, caps?: CapSet): OpenPayload;
60
81
  export declare function decodeHead(b: Uint8Array): HeadPayload;
61
82
  /**
62
- * The map arity is asserted AGAINST THE KIND, so a payload claiming `revoke`
63
- * with a trailing label is refused as malformed rather than quietly accepted
64
- * with the extra key ignored.
83
+ * Decode a CONTROL under the connection's agreed set. EVERY failure is fatal:
84
+ * CONTROL rides stream 0, so there is no stream to reset, and a CONTROL the
85
+ * client cannot read is the edge misbehaving. The map arity is asserted
86
+ * AGAINST THE KIND, so a payload claiming `revoke` with a trailing label is
87
+ * refused rather than quietly accepted with the extra key ignored.
88
+ */
89
+ export declare function decodeControl(b: Uint8Array, caps?: CapSet): ControlPayload;
90
+ /**
91
+ * Canonical key order: viewerLabel, method, path, headers, [body], [upgrade].
92
+ * The client never sends an OPEN; this is the mirror of the edge's encoder for
93
+ * the fixture round trip, and it refuses an element outside `caps` the same way.
65
94
  */
66
- export declare function decodeControl(b: Uint8Array): ControlPayload;
67
- /** Canonical key order: viewerLabel, method, path, headers, [upgrade]. */
68
- export declare function encodeOpen(p: OpenPayload): Uint8Array;
95
+ export declare function encodeOpen(p: OpenPayload, caps?: CapSet): Uint8Array;
69
96
  /** Canonical key order: status, headers. */
70
97
  export declare function encodeHead(p: HeadPayload): Uint8Array;
71
- export declare function encodeControl(kind: ControlKind, viewerLabel?: string): Uint8Array;
98
+ export declare function encodeControl(kind: ControlKind, viewerLabel?: string, caps?: CapSet): Uint8Array;
@@ -18,10 +18,25 @@
18
18
  import { MAX_CBOR_DEPTH, MAX_HEADERS, MAX_HEADER_BYTES, MAX_OPEN_PAYLOAD, MAX_PATH_LEN, ProtocolError, } from "./frame.js";
19
19
  const MAX_METHOD_LEN = 16;
20
20
  const MAX_VIEWER_LABEL_LEN = 11;
21
- /** Every CBOR error is STREAM-level: the stream dies, the tunnel SURVIVES. */
21
+ /** A CBOR shape error is STREAM-level: the stream dies, the tunnel SURVIVES. */
22
22
  function fail(code) {
23
23
  throw new ProtocolError(code, false);
24
24
  }
25
+ /**
26
+ * An element outside the connection's agreed set, or a violation of what the
27
+ * agreement promises, closes the TUNNEL: only the edge writes these keys, so a
28
+ * wrong one is the edge misbehaving, not a viewer.
29
+ */
30
+ function fatal(code) {
31
+ throw new ProtocolError(code, true);
32
+ }
33
+ /**
34
+ * The optional protocol elements a connection may agree on the upgrade (f3
35
+ * §2.2.2). Each changes one disjoint grammar element.
36
+ */
37
+ export const TUNNEL_CAPS = ["open-body", "control-authz-expired"];
38
+ /** The agreed-nothing grammar: exactly the pre-negotiation protocol. */
39
+ export const BASE_CAPS = new Set();
25
40
  /**
26
41
  * VIEWER_LABEL_RE is the SAME regex the edge assigns against, and this client
27
42
  * RE-VALIDATES rather than trusting the edge. That is closure (c) of the three
@@ -61,8 +76,13 @@ const CONTROL_ARITY = new Map([
61
76
  ["revoke", 1],
62
77
  ["expiring", 1],
63
78
  ["superseded", 1],
79
+ ["authz_expired", 1],
64
80
  ["viewer_gone", 2],
65
81
  ]);
82
+ /** The kinds a connection may receive only when it agreed the named element. */
83
+ const NEGOTIATED_KIND = new Map([
84
+ ["authz_expired", "control-authz-expired"],
85
+ ]);
66
86
  // ---- decoding ---------------------------------------------------------------
67
87
  class Reader {
68
88
  b;
@@ -140,6 +160,13 @@ class Reader {
140
160
  fail("cbor_malformed");
141
161
  return arg;
142
162
  }
163
+ /** CBOR simple values 20 (false) and 21 (true); nothing else. */
164
+ bool() {
165
+ const { major, arg } = this.head();
166
+ if (major !== 7 || (arg !== 20 && arg !== 21))
167
+ fail("cbor_malformed");
168
+ return arg === 21;
169
+ }
143
170
  /**
144
171
  * headerPairs reads a map of text->text, enforcing MAX_HEADERS and
145
172
  * MAX_HEADER_BYTES as it goes. The COUNT bound is checked against the
@@ -173,7 +200,12 @@ class Reader {
173
200
  }
174
201
  }
175
202
  }
176
- export function decodeOpen(b) {
203
+ /**
204
+ * Decode an OPEN under the connection's agreed set. `body` on a connection that
205
+ * did not agree `open-body`, its absence on a non-upgrade OPEN that did, and
206
+ * `body` beside `upgrade` are all fatal: only the edge writes that key.
207
+ */
208
+ export function decodeOpen(b, caps = BASE_CAPS) {
177
209
  if (b.length > MAX_OPEN_PAYLOAD)
178
210
  fail("open_payload_too_large");
179
211
  const r = new Reader(b);
@@ -184,6 +216,7 @@ export function decodeOpen(b) {
184
216
  let method;
185
217
  let path;
186
218
  let headers = [];
219
+ let body;
187
220
  let upgrade;
188
221
  for (let k = 0; k < arg; k++) {
189
222
  const key = r.text(32);
@@ -210,6 +243,11 @@ export function decodeOpen(b) {
210
243
  case "headers":
211
244
  headers = r.headerPairs();
212
245
  break;
246
+ case "body":
247
+ if (!caps.has("open-body"))
248
+ fatal("not_negotiated");
249
+ body = r.bool();
250
+ break;
213
251
  case "upgrade":
214
252
  upgrade = r.text(32);
215
253
  break;
@@ -227,7 +265,15 @@ export function decodeOpen(b) {
227
265
  // frame.
228
266
  if (r.i !== b.length)
229
267
  fail("cbor_malformed");
268
+ if (caps.has("open-body")) {
269
+ if (upgrade !== undefined && body !== undefined)
270
+ fatal("open_body_with_upgrade");
271
+ if (upgrade === undefined && body === undefined)
272
+ fatal("open_body_missing");
273
+ }
230
274
  const out = { viewerLabel, method, path, headers };
275
+ if (body !== undefined)
276
+ out.body = body;
231
277
  if (upgrade !== undefined)
232
278
  out.upgrade = upgrade;
233
279
  return out;
@@ -267,11 +313,23 @@ export function decodeHead(b) {
267
313
  return { status, headers };
268
314
  }
269
315
  /**
270
- * The map arity is asserted AGAINST THE KIND, so a payload claiming `revoke`
271
- * with a trailing label is refused as malformed rather than quietly accepted
272
- * with the extra key ignored.
316
+ * Decode a CONTROL under the connection's agreed set. EVERY failure is fatal:
317
+ * CONTROL rides stream 0, so there is no stream to reset, and a CONTROL the
318
+ * client cannot read is the edge misbehaving. The map arity is asserted
319
+ * AGAINST THE KIND, so a payload claiming `revoke` with a trailing label is
320
+ * refused rather than quietly accepted with the extra key ignored.
273
321
  */
274
- export function decodeControl(b) {
322
+ export function decodeControl(b, caps = BASE_CAPS) {
323
+ try {
324
+ return decodeControlShape(b, caps);
325
+ }
326
+ catch (err) {
327
+ if (err instanceof ProtocolError && !err.fatal)
328
+ fatal(err.code);
329
+ throw err;
330
+ }
331
+ }
332
+ function decodeControlShape(b, caps) {
275
333
  if (b.length > MAX_OPEN_PAYLOAD)
276
334
  fail("open_payload_too_large");
277
335
  const r = new Reader(b);
@@ -281,6 +339,9 @@ export function decodeControl(b) {
281
339
  if (r.text(32) !== "kind")
282
340
  fail("cbor_malformed");
283
341
  const v = r.text(32);
342
+ const needs = NEGOTIATED_KIND.get(v);
343
+ if (needs !== undefined && !caps.has(needs))
344
+ fatal("not_negotiated");
284
345
  const want = CONTROL_ARITY.get(v);
285
346
  if (want === undefined || arg !== want)
286
347
  fail("cbor_malformed");
@@ -347,20 +408,30 @@ function headerMap(h) {
347
408
  out = out.concat(text(k), text(v));
348
409
  return out;
349
410
  }
350
- /** Canonical key order: viewerLabel, method, path, headers, [upgrade]. */
351
- export function encodeOpen(p) {
411
+ /**
412
+ * Canonical key order: viewerLabel, method, path, headers, [body], [upgrade].
413
+ * The client never sends an OPEN; this is the mirror of the edge's encoder for
414
+ * the fixture round trip, and it refuses an element outside `caps` the same way.
415
+ */
416
+ export function encodeOpen(p, caps = BASE_CAPS) {
352
417
  if (parseViewerLabel(p.viewerLabel) === null)
353
418
  fail("cbor_viewer");
354
419
  if (p.path.length > MAX_PATH_LEN)
355
420
  fail("cbor_path_len");
356
421
  if (p.headers.length > MAX_HEADERS)
357
422
  fail("cbor_headers");
358
- const n = p.upgrade ? 5 : 4;
423
+ if (p.body !== undefined && !caps.has("open-body"))
424
+ fatal("not_negotiated");
425
+ if (p.body !== undefined && p.upgrade)
426
+ fatal("open_body_with_upgrade");
427
+ const n = 4 + (p.body !== undefined ? 1 : 0) + (p.upgrade ? 1 : 0);
359
428
  let out = head(5, n);
360
429
  out = out.concat(text("viewerLabel"), text(p.viewerLabel));
361
430
  out = out.concat(text("method"), text(p.method));
362
431
  out = out.concat(text("path"), text(p.path));
363
432
  out = out.concat(text("headers"), headerMap(p.headers));
433
+ if (p.body !== undefined)
434
+ out = out.concat(text("body"), [p.body ? 0xf5 : 0xf4]);
364
435
  if (p.upgrade)
365
436
  out = out.concat(text("upgrade"), text(p.upgrade));
366
437
  const b = new Uint8Array(out);
@@ -382,10 +453,13 @@ export function encodeHead(p) {
382
453
  fail("open_payload_too_large");
383
454
  return b;
384
455
  }
385
- export function encodeControl(kind, viewerLabel) {
456
+ export function encodeControl(kind, viewerLabel, caps = BASE_CAPS) {
386
457
  const arity = CONTROL_ARITY.get(kind);
387
458
  if (arity === undefined)
388
459
  fail("cbor_malformed");
460
+ const needs = NEGOTIATED_KIND.get(kind);
461
+ if (needs !== undefined && !caps.has(needs))
462
+ fatal("not_negotiated");
389
463
  if (kind === "viewer_gone") {
390
464
  if (viewerLabel === undefined || parseViewerLabel(viewerLabel) === null) {
391
465
  fail("cbor_viewer");
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { WebSocket } from "ws";
16
16
  import { encodeCredit } from "./frame.js";
17
- import { type ControlKind, type UAClass } from "./cbor.js";
17
+ import { type CapSet, type ControlKind, type OpenPayload, type UAClass } from "./cbor.js";
18
18
  import { type ForwardTarget } from "./forwardTarget.js";
19
19
  /**
20
20
  * VIEWER_STAMP_HEADER carries the VALIDATED label to the creator's own local
@@ -59,6 +59,15 @@ export declare const RESUME_HEADER = "x-vincentt-resume";
59
59
  export declare const SESSION_HEADER = "x-vincentt-session";
60
60
  /** The Authorization header, named once so the never-send-both rule cites it. */
61
61
  export declare const AUTHORIZATION_HEADER = "authorization";
62
+ /**
63
+ * The version refusal on a dial: `410` with these headers (f3 §2.2.3). 410 is
64
+ * in a released client's stop set; only a client that reads the reason header
65
+ * knows it means "upgrade".
66
+ */
67
+ export declare const REFUSAL_HEADER = "x-vincentt-refusal";
68
+ export declare const CLI_MINIMUM_HEADER = "x-vincentt-cli-minimum";
69
+ export declare const CLI_DECLARED_HEADER = "x-vincentt-cli-declared";
70
+ export declare const UPGRADE_REQUIRED_REFUSAL = "cli_upgrade_required";
62
71
  /**
63
72
  * RESUME_TOKEN_LEN is base64url(32 random bytes), unpadded. It mirrors
64
73
  * `resumeTokenLen` in the Go edge so the two ends of this wire state the same
@@ -113,8 +122,17 @@ export interface TunnelClientOptions {
113
122
  * client nothing, so a phone refreshing read as an arriving crowd.
114
123
  */
115
124
  readonly onViewerGone?: (label: string) => void;
116
- /** onControl carries revoke/expiring/superseded so the CLI prints the right ending. */
125
+ /**
126
+ * onControl carries revoke/expiring/superseded/authz_expired so the CLI prints
127
+ * the right ending. `authz_expired` arrives only on a connection that agreed
128
+ * it, and the client has already stopped redialling when it fires.
129
+ */
117
130
  readonly onControl?: (kind: ControlKind) => void;
131
+ /**
132
+ * The edge refused the dial for this CLI's version. Raw header values: the
133
+ * caller validates them. The client makes no further dial after this.
134
+ */
135
+ readonly onRefused?: (reason: "upgrade_required", minimum: string | undefined, declared: string | undefined) => void;
118
136
  /** onOriginError is the creator-facing detail the 502 page must never carry. */
119
137
  readonly onOriginError?: (label: string, code: string) => void;
120
138
  readonly onStateChange?: (state: TunnelState) => void;
@@ -143,6 +161,27 @@ export interface TunnelClientOptions {
143
161
  readonly connect?: (url: string, headers: Record<string, string>) => WebSocket;
144
162
  }
145
163
  export type TunnelState = "connecting" | "live" | "reconnecting" | "closed";
164
+ /**
165
+ * What to do with a request's body. `reset` is a disagreement a viewer can
166
+ * cause, so it ends that stream and never the tunnel.
167
+ */
168
+ export type BodyPlan = {
169
+ kind: "none";
170
+ } | {
171
+ kind: "declared";
172
+ length: number;
173
+ } | {
174
+ kind: "chunked";
175
+ } | {
176
+ kind: "reset";
177
+ };
178
+ /**
179
+ * With `open-body` agreed, the edge's `body` decides and a content-length must
180
+ * agree with it (the edge writes at most one, positive). Without it, the base
181
+ * rule: a positive content-length streams, anything else ends at OPEN. An
182
+ * upgrade has no body whatever it declares.
183
+ */
184
+ export declare function bodyPlan(open: OpenPayload, caps: CapSet): BodyPlan;
146
185
  export declare class TunnelClient {
147
186
  private readonly opts;
148
187
  private ws;
@@ -155,6 +194,8 @@ export declare class TunnelClient {
155
194
  * is a credential in a log.
156
195
  */
157
196
  private resumeToken;
197
+ /** This connection's agreed set. Fixed at its 101, reset on every dial. */
198
+ private caps;
158
199
  private attempt;
159
200
  private closed;
160
201
  private state;
@@ -190,6 +231,11 @@ export declare class TunnelClient {
190
231
  private dialHeaders;
191
232
  private open;
192
233
  private onClosed;
234
+ /**
235
+ * Close this connection for a violation the edge alone could cause. The
236
+ * ordinary close path then decides whether to redial.
237
+ */
238
+ private protocolViolation;
193
239
  private startPing;
194
240
  private stopPing;
195
241
  private send;