@debugg-ai/debugg-ai-mcp 4.2.2 → 4.4.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 (34) hide show
  1. package/README.md +10 -4
  2. package/dist/handlers/createEnvironmentHandler.js +10 -0
  3. package/dist/handlers/probePageHandler.js +14 -7
  4. package/dist/handlers/runTestSuiteHandler.js +6 -5
  5. package/dist/handlers/testPageChangesHandler.js +20 -10
  6. package/dist/handlers/triggerCrawlHandler.js +7 -6
  7. package/dist/handlers/updateEnvironmentHandler.js +14 -2
  8. package/dist/services/index.js +21 -0
  9. package/dist/services/ngrok/tunnelManager.js +157 -161
  10. package/dist/services/tunnel/debuggTransport.js +439 -0
  11. package/dist/services/tunnel/ngrokTransport.js +165 -0
  12. package/dist/services/tunnel/probeRegistry.js +52 -0
  13. package/dist/services/tunnel/protocol/codec.js +327 -0
  14. package/dist/services/tunnel/protocol/conformance.js +171 -0
  15. package/dist/services/tunnel/protocol/constants.js +178 -0
  16. package/dist/services/tunnel/protocol/errors.js +51 -0
  17. package/dist/services/tunnel/protocol/handshake.js +134 -0
  18. package/dist/services/tunnel/protocol/index.js +17 -0
  19. package/dist/services/tunnel/protocol/session.js +668 -0
  20. package/dist/services/tunnel/protocol/stream.js +109 -0
  21. package/dist/services/tunnel/protocol/transport.js +106 -0
  22. package/dist/services/tunnel/transport.js +15 -0
  23. package/dist/services/tunnels.js +71 -3
  24. package/dist/services/verdictAdapter.js +25 -1
  25. package/dist/tools/environment.js +5 -2
  26. package/dist/tools/testPageChanges.js +1 -1
  27. package/dist/types/index.js +29 -2
  28. package/dist/utils/authorizedCredentialHosts.js +38 -0
  29. package/dist/utils/localReachability.js +160 -3
  30. package/dist/utils/tunnelContext.js +6 -3
  31. package/dist/utils/tunnelDisposition.js +16 -1
  32. package/dist/utils/tunnelDomains.js +113 -0
  33. package/dist/utils/urlParser.js +8 -2
  34. package/package.json +3 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Debugg tunnel wire protocol v1 — error types.
3
+ *
4
+ * STUB (bead debugg_ai_mcp-xkoh.1.3, phase 2.1): these are data-only classes, so
5
+ * they are complete; every behavioural module in this directory is a stub that
6
+ * throws until phase 4.1.
7
+ */
8
+ import { ErrorCode } from './constants.js';
9
+ /**
10
+ * The peer (or our own encoder's caller) violated the wire spec. Always carries
11
+ * ErrorCode.PROTOCOL_ERROR — the code exists so callers can treat it uniformly
12
+ * with StreamResetError.
13
+ */
14
+ export class ProtocolError extends Error {
15
+ code = ErrorCode.PROTOCOL_ERROR;
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'ProtocolError';
19
+ }
20
+ }
21
+ /**
22
+ * A stream ended early. `code` is an ErrorCode value; an unknown code seen on
23
+ * the wire is normalised to ErrorCode.RESET before it gets here. `remote` is
24
+ * true when the peer sent the RST, false when this side originated it (a cap
25
+ * refusal, a GOAWAY drain deadline).
26
+ */
27
+ export class StreamResetError extends Error {
28
+ code;
29
+ remote;
30
+ constructor(code, message, remote = false) {
31
+ super(message ?? `tunnel stream reset (code ${code})`);
32
+ this.name = 'StreamResetError';
33
+ this.code = code;
34
+ this.remote = remote;
35
+ }
36
+ }
37
+ /**
38
+ * The session is gone: the websocket closed, the peer went silent, or a fatal
39
+ * protocol error tore it down. `closeCode` is the websocket close code
40
+ * (1000, or 4000 + ErrorCode, or 1006 for an abrupt socket loss).
41
+ */
42
+ export class SessionClosedError extends Error {
43
+ closeCode;
44
+ reason;
45
+ constructor(closeCode, reason = '') {
46
+ super(`tunnel session closed (${closeCode}${reason ? `: ${reason}` : ''})`);
47
+ this.name = 'SessionClosedError';
48
+ this.closeCode = closeCode;
49
+ this.reason = reason;
50
+ }
51
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Debugg tunnel wire protocol v1 — the websocket handshake.
3
+ *
4
+ * This module validates the SHAPE of a handshake only. Whether a tunnelKey is
5
+ * real is the tunnel server's business (it asks the debugg API's internal verify
6
+ * endpoint) — the protocol just guarantees both ends agree on how the question
7
+ * is asked and how a refusal is phrased.
8
+ *
9
+ * Check order is version (426) -> tunnel id (400) -> authorization (401), so an
10
+ * old client is told to upgrade even when its token is also bad.
11
+ */
12
+ import { HandshakeError, HandshakeStatus, Header, RequestHeader, SUPPORTED_PROTOCOL_VERSIONS, TUNNEL_ID_PATTERN, } from './constants.js';
13
+ const JSON_CONTENT_TYPE = { 'Content-Type': 'application/json' };
14
+ function headerValue(headers, name) {
15
+ const raw = headers[name] ?? headers[name.toLowerCase()];
16
+ if (raw === undefined)
17
+ return undefined;
18
+ return Array.isArray(raw) ? raw.join(', ') : raw;
19
+ }
20
+ /** Build the request headers a client sends with its upgrade request. */
21
+ export function buildHandshakeHeaders(request) {
22
+ const versions = request.versions ?? SUPPORTED_PROTOCOL_VERSIONS;
23
+ const headers = {
24
+ [RequestHeader.AUTHORIZATION]: `Bearer ${request.tunnelKey}`,
25
+ [RequestHeader.TUNNEL_ID]: request.tunnelId,
26
+ [RequestHeader.PROTOCOL]: versions.join(', '),
27
+ };
28
+ if (request.clientVersion)
29
+ headers[RequestHeader.CLIENT_VERSION] = request.clientVersion;
30
+ return headers;
31
+ }
32
+ /**
33
+ * Pick the highest version both ends speak from an X-Debugg-Tunnel-Protocol
34
+ * header value, or null when there is none. Tolerates whitespace, a repeated
35
+ * header and versions we don't know; anything non-numeric is simply not a
36
+ * version and is ignored.
37
+ */
38
+ export function negotiateProtocolVersion(offered, supported = SUPPORTED_PROTOCOL_VERSIONS) {
39
+ if (offered === undefined)
40
+ return null;
41
+ const tokens = (Array.isArray(offered) ? offered.join(',') : offered).split(',');
42
+ let best = null;
43
+ for (const token of tokens) {
44
+ const trimmed = token.trim();
45
+ if (!/^\d+$/.test(trimmed))
46
+ continue;
47
+ const version = Number(trimmed);
48
+ if (!supported.includes(version))
49
+ continue;
50
+ if (best === null || version > best)
51
+ best = version;
52
+ }
53
+ return best;
54
+ }
55
+ /**
56
+ * Server side: validate an upgrade request's headers.
57
+ */
58
+ export function validateHandshakeRequest(headers, supported = SUPPORTED_PROTOCOL_VERSIONS) {
59
+ const version = negotiateProtocolVersion(headerValue(headers, Header.PROTOCOL), supported);
60
+ if (version === null) {
61
+ return {
62
+ ok: false,
63
+ status: HandshakeStatus.UPGRADE_REQUIRED,
64
+ error: HandshakeError.UNSUPPORTED_PROTOCOL_VERSION,
65
+ responseHeaders: { [RequestHeader.SUPPORTED]: supported.join(', '), ...JSON_CONTENT_TYPE },
66
+ body: { error: HandshakeError.UNSUPPORTED_PROTOCOL_VERSION, supported: [...supported] },
67
+ };
68
+ }
69
+ const tunnelId = headerValue(headers, Header.TUNNEL_ID);
70
+ if (!tunnelId || !TUNNEL_ID_PATTERN.test(tunnelId)) {
71
+ return {
72
+ ok: false,
73
+ status: HandshakeStatus.BAD_REQUEST,
74
+ error: HandshakeError.BAD_TUNNEL_ID,
75
+ responseHeaders: { ...JSON_CONTENT_TYPE },
76
+ body: { error: HandshakeError.BAD_TUNNEL_ID },
77
+ };
78
+ }
79
+ const authorization = headerValue(headers, Header.AUTHORIZATION);
80
+ const bearer = authorization ? /^bearer\s+(\S.*)$/i.exec(authorization) : null;
81
+ if (!bearer) {
82
+ return {
83
+ ok: false,
84
+ status: HandshakeStatus.UNAUTHORIZED,
85
+ error: HandshakeError.UNAUTHORIZED,
86
+ responseHeaders: { ...JSON_CONTENT_TYPE },
87
+ body: { error: HandshakeError.UNAUTHORIZED },
88
+ };
89
+ }
90
+ const accepted = {
91
+ ok: true,
92
+ version,
93
+ tunnelId,
94
+ tunnelKey: bearer[1].trim(),
95
+ responseHeaders: { [RequestHeader.PROTOCOL]: String(version) },
96
+ };
97
+ const clientVersion = headerValue(headers, Header.CLIENT_VERSION);
98
+ if (clientVersion)
99
+ accepted.clientVersion = clientVersion;
100
+ return accepted;
101
+ }
102
+ function parseVersionList(value) {
103
+ if (value === undefined)
104
+ return undefined;
105
+ const versions = value
106
+ .split(',')
107
+ .map((token) => token.trim())
108
+ .filter((token) => /^\d+$/.test(token))
109
+ .map(Number);
110
+ return versions.length > 0 ? versions : undefined;
111
+ }
112
+ /** Client side: interpret whatever came back instead of (or alongside) a 101. */
113
+ export function interpretHandshakeResponse(status, headers, offered = SUPPORTED_PROTOCOL_VERSIONS) {
114
+ if (status === HandshakeStatus.SWITCHING_PROTOCOLS) {
115
+ const echoed = negotiateProtocolVersion(headerValue(headers, Header.PROTOCOL), offered);
116
+ if (echoed === null) {
117
+ // No echo, or a version we never offered: the peer is not speaking a
118
+ // protocol we understand, and pretending otherwise corrupts the stream.
119
+ return { ok: false, status, retryable: false };
120
+ }
121
+ return { ok: true, version: echoed };
122
+ }
123
+ const failure = {
124
+ ok: false,
125
+ status,
126
+ retryable: status === 429 || status >= 500,
127
+ };
128
+ if (status === HandshakeStatus.UPGRADE_REQUIRED) {
129
+ const supported = parseVersionList(headerValue(headers, Header.SUPPORTED));
130
+ if (supported)
131
+ failure.supported = supported;
132
+ }
133
+ return failure;
134
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Debugg tunnel wire protocol v1 — public surface.
3
+ *
4
+ * The MCP tunnel client (services/tunnel/) and the debugg tunnel server both
5
+ * import from here and nowhere deeper. Conformance vectors for any copy of this
6
+ * module live in ./vectors/.
7
+ *
8
+ * Spec: bead debugg_ai_mcp-xkoh.1.2. Design: docs/debugg-tunnel-server-design-2026-09-19.md §3.
9
+ */
10
+ export * from './constants.js';
11
+ export * from './errors.js';
12
+ export * from './codec.js';
13
+ export * from './transport.js';
14
+ export * from './stream.js';
15
+ export * from './session.js';
16
+ export * from './handshake.js';
17
+ export * from './conformance.js';