@graphorin/protocol 0.6.1 → 0.7.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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Subprotocol identifier + negotiation helpers for the Graphorin
3
+ * WebSocket protocol.
4
+ *
5
+ * Clients announce supported subprotocols in the
6
+ * `Sec-WebSocket-Protocol` upgrade header. The server is expected to
7
+ * pick exactly one and echo it back; mismatches abort the handshake
8
+ * per RFC 6455 § 4. Browsers also accept additional comma-separated
9
+ * tokens - Graphorin uses this slot to attach a single-use ticket
10
+ * via the `ticket.<value>` form (the WebSocket browser API does not
11
+ * accept arbitrary headers, so the ticket has to ride the
12
+ * subprotocol channel).
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ /**
18
+ * Canonical subprotocol identifier for the v1 wire format.
19
+ *
20
+ * @stable
21
+ */
22
+ export const SUBPROTOCOL_NAME = 'graphorin.protocol.v1';
23
+
24
+ /**
25
+ * Wire-format major version literal carried on every message body.
26
+ * The pair `(SUBPROTOCOL_NAME, PROTOCOL_VERSION)` is the binding
27
+ * contract a client commits to when it receives a successful upgrade.
28
+ *
29
+ * @stable
30
+ */
31
+ export const PROTOCOL_VERSION = '1';
32
+
33
+ /**
34
+ * Prefix for the single-use ticket that browser clients attach to
35
+ * the `Sec-WebSocket-Protocol` header. The server's upgrade handler
36
+ * splits the comma-separated list, finds the first
37
+ * `ticket.<value>` token, and validates the value against the
38
+ * in-memory ticket store before granting the connection.
39
+ *
40
+ * @stable
41
+ */
42
+ export const TICKET_SUBPROTOCOL_PREFIX = 'ticket.';
43
+
44
+ /**
45
+ * Format a ticket value as a `Sec-WebSocket-Protocol` token suitable
46
+ * for browser clients (which cannot attach an `Authorization`
47
+ * header on the WebSocket upgrade). The companion server helper is
48
+ * {@link parseTicketSubprotocol}.
49
+ *
50
+ * @stable
51
+ */
52
+ export function formatTicketSubprotocol(ticket: string): string {
53
+ if (typeof ticket !== 'string' || ticket.length === 0) {
54
+ throw new TypeError('formatTicketSubprotocol: ticket must be a non-empty string.');
55
+ }
56
+ return `${TICKET_SUBPROTOCOL_PREFIX}${ticket}`;
57
+ }
58
+
59
+ /**
60
+ * Extract the ticket value from a single comma-separated client list
61
+ * (e.g. `'graphorin.protocol.v1, ticket.abc-123'`). Returns
62
+ * `undefined` if no `ticket.*` token is present. Whitespace around
63
+ * each comma-separated token is ignored.
64
+ *
65
+ * @stable
66
+ */
67
+ export function parseTicketSubprotocol(
68
+ clientList: string | ReadonlyArray<string>,
69
+ ): string | undefined {
70
+ for (const token of normalizeTokens(clientList)) {
71
+ if (token.startsWith(TICKET_SUBPROTOCOL_PREFIX)) {
72
+ const value = token.slice(TICKET_SUBPROTOCOL_PREFIX.length);
73
+ if (value.length > 0) return value;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ /**
80
+ * Pick the single subprotocol the server should echo back. Returns
81
+ * `SUBPROTOCOL_NAME` when the client offered it, or `null` when no
82
+ * compatible variant was advertised. The function ignores `ticket.*`
83
+ * tokens - those are handled separately via {@link parseTicketSubprotocol}.
84
+ *
85
+ * @stable
86
+ */
87
+ export function negotiateSubprotocol(clientList: string | ReadonlyArray<string>): string | null {
88
+ for (const token of normalizeTokens(clientList)) {
89
+ if (token === SUBPROTOCOL_NAME) return SUBPROTOCOL_NAME;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function normalizeTokens(input: string | ReadonlyArray<string>): ReadonlyArray<string> {
95
+ if (Array.isArray(input)) {
96
+ return input
97
+ .flatMap((entry) => entry.split(','))
98
+ .map((token) => token.trim())
99
+ .filter((token) => token.length > 0);
100
+ }
101
+ if (typeof input !== 'string') return [];
102
+ return input
103
+ .split(',')
104
+ .map((token) => token.trim())
105
+ .filter((token) => token.length > 0);
106
+ }