@ttctl/core 0.0.0 → 0.1.0-rc.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.
Files changed (195) hide show
  1. package/README.md +49 -9
  2. package/dist/__generated__/gateway.d.ts +4546 -0
  3. package/dist/__generated__/gateway.d.ts.map +1 -0
  4. package/dist/__generated__/gateway.js +9 -0
  5. package/dist/__generated__/gateway.js.map +1 -0
  6. package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
  7. package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
  8. package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
  9. package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
  10. package/dist/__generated__/talent-profile.d.ts +1397 -0
  11. package/dist/__generated__/talent-profile.d.ts.map +1 -0
  12. package/dist/__generated__/talent-profile.js +9 -0
  13. package/dist/__generated__/talent-profile.js.map +1 -0
  14. package/dist/__generated__/zod-schemas.d.ts +2895 -0
  15. package/dist/__generated__/zod-schemas.d.ts.map +1 -0
  16. package/dist/__generated__/zod-schemas.js +3121 -0
  17. package/dist/__generated__/zod-schemas.js.map +1 -0
  18. package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
  19. package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/profile/builders.js +196 -0
  21. package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
  22. package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
  23. package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
  24. package/dist/__tests__/fixtures/profile/data.js +230 -0
  25. package/dist/__tests__/fixtures/profile/data.js.map +1 -0
  26. package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
  27. package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
  28. package/dist/__tests__/fixtures/profile/index.js +10 -0
  29. package/dist/__tests__/fixtures/profile/index.js.map +1 -0
  30. package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
  31. package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
  32. package/dist/__tests__/fixtures/profile/types.js +4 -0
  33. package/dist/__tests__/fixtures/profile/types.js.map +1 -0
  34. package/dist/auth/errors.d.ts +82 -0
  35. package/dist/auth/errors.d.ts.map +1 -0
  36. package/dist/auth/errors.js +68 -0
  37. package/dist/auth/errors.js.map +1 -0
  38. package/dist/auth.d.ts +192 -0
  39. package/dist/auth.d.ts.map +1 -0
  40. package/dist/auth.js +294 -0
  41. package/dist/auth.js.map +1 -0
  42. package/dist/config.d.ts +212 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +349 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/configLock.d.ts +50 -0
  47. package/dist/configLock.d.ts.map +1 -0
  48. package/dist/configLock.js +88 -0
  49. package/dist/configLock.js.map +1 -0
  50. package/dist/configWriter.d.ts +97 -0
  51. package/dist/configWriter.d.ts.map +1 -0
  52. package/dist/configWriter.js +687 -0
  53. package/dist/configWriter.js.map +1 -0
  54. package/dist/index.d.ts +37 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +28 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/kill-switch.d.ts +161 -0
  59. package/dist/kill-switch.d.ts.map +1 -0
  60. package/dist/kill-switch.js +235 -0
  61. package/dist/kill-switch.js.map +1 -0
  62. package/dist/lib/date.d.ts +58 -0
  63. package/dist/lib/date.d.ts.map +1 -0
  64. package/dist/lib/date.js +104 -0
  65. package/dist/lib/date.js.map +1 -0
  66. package/dist/lib/diagnostic-log.d.ts +159 -0
  67. package/dist/lib/diagnostic-log.d.ts.map +1 -0
  68. package/dist/lib/diagnostic-log.js +186 -0
  69. package/dist/lib/diagnostic-log.js.map +1 -0
  70. package/dist/lib/package-version.d.ts +19 -0
  71. package/dist/lib/package-version.d.ts.map +1 -0
  72. package/dist/lib/package-version.js +38 -0
  73. package/dist/lib/package-version.js.map +1 -0
  74. package/dist/lib/redact.d.ts +153 -0
  75. package/dist/lib/redact.d.ts.map +1 -0
  76. package/dist/lib/redact.js +207 -0
  77. package/dist/lib/redact.js.map +1 -0
  78. package/dist/lib/text.d.ts +14 -0
  79. package/dist/lib/text.d.ts.map +1 -0
  80. package/dist/lib/text.js +21 -0
  81. package/dist/lib/text.js.map +1 -0
  82. package/dist/lib/wire-shape.d.ts +131 -0
  83. package/dist/lib/wire-shape.d.ts.map +1 -0
  84. package/dist/lib/wire-shape.js +376 -0
  85. package/dist/lib/wire-shape.js.map +1 -0
  86. package/dist/onepassword.d.ts +29 -0
  87. package/dist/onepassword.d.ts.map +1 -0
  88. package/dist/onepassword.js +112 -0
  89. package/dist/onepassword.js.map +1 -0
  90. package/dist/services/_shared/transport.d.ts +148 -0
  91. package/dist/services/_shared/transport.d.ts.map +1 -0
  92. package/dist/services/_shared/transport.js +102 -0
  93. package/dist/services/_shared/transport.js.map +1 -0
  94. package/dist/services/applications/index.d.ts +210 -0
  95. package/dist/services/applications/index.d.ts.map +1 -0
  96. package/dist/services/applications/index.js +240 -0
  97. package/dist/services/applications/index.js.map +1 -0
  98. package/dist/services/availability/index.d.ts +254 -0
  99. package/dist/services/availability/index.d.ts.map +1 -0
  100. package/dist/services/availability/index.js +310 -0
  101. package/dist/services/availability/index.js.map +1 -0
  102. package/dist/services/contracts/index.d.ts +132 -0
  103. package/dist/services/contracts/index.d.ts.map +1 -0
  104. package/dist/services/contracts/index.js +211 -0
  105. package/dist/services/contracts/index.js.map +1 -0
  106. package/dist/services/engagements/index.d.ts +504 -0
  107. package/dist/services/engagements/index.d.ts.map +1 -0
  108. package/dist/services/engagements/index.js +613 -0
  109. package/dist/services/engagements/index.js.map +1 -0
  110. package/dist/services/jobs/index.d.ts +490 -0
  111. package/dist/services/jobs/index.d.ts.map +1 -0
  112. package/dist/services/jobs/index.js +753 -0
  113. package/dist/services/jobs/index.js.map +1 -0
  114. package/dist/services/payments/index.d.ts +415 -0
  115. package/dist/services/payments/index.d.ts.map +1 -0
  116. package/dist/services/payments/index.js +636 -0
  117. package/dist/services/payments/index.js.map +1 -0
  118. package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
  119. package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
  120. package/dist/services/profile/__tests__/fixtures.js +176 -0
  121. package/dist/services/profile/__tests__/fixtures.js.map +1 -0
  122. package/dist/services/profile/basic/index.d.ts +390 -0
  123. package/dist/services/profile/basic/index.d.ts.map +1 -0
  124. package/dist/services/profile/basic/index.js +1007 -0
  125. package/dist/services/profile/basic/index.js.map +1 -0
  126. package/dist/services/profile/certifications/index.d.ts +74 -0
  127. package/dist/services/profile/certifications/index.d.ts.map +1 -0
  128. package/dist/services/profile/certifications/index.js +169 -0
  129. package/dist/services/profile/certifications/index.js.map +1 -0
  130. package/dist/services/profile/education/index.d.ts +73 -0
  131. package/dist/services/profile/education/index.d.ts.map +1 -0
  132. package/dist/services/profile/education/index.js +168 -0
  133. package/dist/services/profile/education/index.js.map +1 -0
  134. package/dist/services/profile/employment/index.d.ts +111 -0
  135. package/dist/services/profile/employment/index.d.ts.map +1 -0
  136. package/dist/services/profile/employment/index.js +202 -0
  137. package/dist/services/profile/employment/index.js.map +1 -0
  138. package/dist/services/profile/external/index.d.ts +219 -0
  139. package/dist/services/profile/external/index.d.ts.map +1 -0
  140. package/dist/services/profile/external/index.js +560 -0
  141. package/dist/services/profile/external/index.js.map +1 -0
  142. package/dist/services/profile/index.d.ts +24 -0
  143. package/dist/services/profile/index.d.ts.map +1 -0
  144. package/dist/services/profile/index.js +26 -0
  145. package/dist/services/profile/index.js.map +1 -0
  146. package/dist/services/profile/industries/index.d.ts +130 -0
  147. package/dist/services/profile/industries/index.d.ts.map +1 -0
  148. package/dist/services/profile/industries/index.js +292 -0
  149. package/dist/services/profile/industries/index.js.map +1 -0
  150. package/dist/services/profile/portfolio/index.d.ts +352 -0
  151. package/dist/services/profile/portfolio/index.d.ts.map +1 -0
  152. package/dist/services/profile/portfolio/index.js +833 -0
  153. package/dist/services/profile/portfolio/index.js.map +1 -0
  154. package/dist/services/profile/resume/index.d.ts +60 -0
  155. package/dist/services/profile/resume/index.d.ts.map +1 -0
  156. package/dist/services/profile/resume/index.js +212 -0
  157. package/dist/services/profile/resume/index.js.map +1 -0
  158. package/dist/services/profile/reviews/index.d.ts +137 -0
  159. package/dist/services/profile/reviews/index.d.ts.map +1 -0
  160. package/dist/services/profile/reviews/index.js +431 -0
  161. package/dist/services/profile/reviews/index.js.map +1 -0
  162. package/dist/services/profile/shared.d.ts +127 -0
  163. package/dist/services/profile/shared.d.ts.map +1 -0
  164. package/dist/services/profile/shared.js +155 -0
  165. package/dist/services/profile/shared.js.map +1 -0
  166. package/dist/services/profile/skills/index.d.ts +212 -0
  167. package/dist/services/profile/skills/index.d.ts.map +1 -0
  168. package/dist/services/profile/skills/index.js +461 -0
  169. package/dist/services/profile/skills/index.js.map +1 -0
  170. package/dist/services/profile/visas/index.d.ts +74 -0
  171. package/dist/services/profile/visas/index.d.ts.map +1 -0
  172. package/dist/services/profile/visas/index.js +306 -0
  173. package/dist/services/profile/visas/index.js.map +1 -0
  174. package/dist/services/timesheet/index.d.ts +326 -0
  175. package/dist/services/timesheet/index.d.ts.map +1 -0
  176. package/dist/services/timesheet/index.js +324 -0
  177. package/dist/services/timesheet/index.js.map +1 -0
  178. package/dist/services/translations.d.ts +79 -0
  179. package/dist/services/translations.d.ts.map +1 -0
  180. package/dist/services/translations.js +136 -0
  181. package/dist/services/translations.js.map +1 -0
  182. package/dist/transport-resilience.d.ts +136 -0
  183. package/dist/transport-resilience.d.ts.map +1 -0
  184. package/dist/transport-resilience.js +247 -0
  185. package/dist/transport-resilience.js.map +1 -0
  186. package/dist/transport.d.ts +408 -0
  187. package/dist/transport.d.ts.map +1 -0
  188. package/dist/transport.js +691 -0
  189. package/dist/transport.js.map +1 -0
  190. package/dist/types.d.ts +41 -0
  191. package/dist/types.d.ts.map +1 -0
  192. package/dist/types.js +18 -0
  193. package/dist/types.js.map +1 -0
  194. package/package.json +40 -12
  195. package/index.js +0 -7
@@ -0,0 +1,136 @@
1
+ import { TtctlError } from "./auth/errors.js";
2
+ import type { ToptalSurface } from "./types.js";
3
+ /**
4
+ * Final-failure transport error raised after all retries are exhausted or
5
+ * when a fatal signal-driven condition (caller abort, internal timeout)
6
+ * terminates the request loop.
7
+ *
8
+ * Extends {@link TtctlError} so it propagates through service callers'
9
+ * `if (err instanceof TtctlError) throw err;` guard verbatim — surfaces
10
+ * (CLI / MCP) render `error.recovery` directly.
11
+ *
12
+ * **Codes**:
13
+ *
14
+ * - `TIMEOUT` — internal per-request timeout fired (default 30s,
15
+ * overridable via `TTCTL_TRANSPORT_TIMEOUT_MS`). The
16
+ * underlying socket/handshake never completed in time.
17
+ * - `ABORTED` — caller's `AbortSignal` aborted before the request
18
+ * completed. Typically driven by the MCP client cancelling
19
+ * an in-flight tool call.
20
+ * - `RATE_LIMITED` — HTTP 429 returned by the surface and the retry budget
21
+ * was exhausted. `lastRetryAfterMs` (when set) reports
22
+ * the server's last `Retry-After` hint.
23
+ * - `SERVER_ERROR` — HTTP 5xx returned by the surface and the retry budget
24
+ * was exhausted.
25
+ */
26
+ export declare class TransportError extends TtctlError {
27
+ readonly surface: ToptalSurface;
28
+ readonly endpoint: string;
29
+ readonly attempts: number;
30
+ readonly lastStatus?: number | undefined;
31
+ readonly lastRetryAfterMs?: number | undefined;
32
+ readonly name = "TransportError";
33
+ readonly code: "TIMEOUT" | "ABORTED" | "RATE_LIMITED" | "SERVER_ERROR";
34
+ readonly recovery: string;
35
+ constructor(code: "TIMEOUT" | "ABORTED" | "RATE_LIMITED" | "SERVER_ERROR", surface: ToptalSurface, endpoint: string, attempts: number, message: string, lastStatus?: number | undefined, lastRetryAfterMs?: number | undefined, options?: {
36
+ cause?: unknown;
37
+ });
38
+ }
39
+ /**
40
+ * Transport resilience configuration. All fields are read from environment
41
+ * variables ONCE on first call to {@link readTransportConfig} and cached. The
42
+ * cache is reset by {@link resetTransportConfigCache} (test-only).
43
+ *
44
+ * Env-var resolution:
45
+ *
46
+ * | Field | Env var | Default | Bounds |
47
+ * |----------------|--------------------------------------|---------|----------|
48
+ * | `timeoutMs` | `TTCTL_TRANSPORT_TIMEOUT_MS` | 30000 | ≥ 1 |
49
+ * | `connectMs` | `TTCTL_TRANSPORT_CONNECT_TIMEOUT_MS` | 10000 | ≥ 1 |
50
+ * | `maxRetries` | `TTCTL_TRANSPORT_MAX_RETRIES` | 3 | 0–10 |
51
+ *
52
+ * Invalid values (NaN, negative, out-of-range) silently fall back to the
53
+ * default — the transport must not blow up on a stray operator typo in an
54
+ * MCP client env block.
55
+ */
56
+ export interface TransportConfig {
57
+ /** Total request timeout in milliseconds applied to each attempt. */
58
+ timeoutMs: number;
59
+ /** Connection-establishment timeout in milliseconds. */
60
+ connectMs: number;
61
+ /** Maximum number of retry attempts for 429 and 5xx responses. */
62
+ maxRetries: number;
63
+ }
64
+ export declare function readTransportConfig(): TransportConfig;
65
+ /**
66
+ * Reset the cached transport config so the next {@link readTransportConfig}
67
+ * call re-reads the environment. Test-only — production callers never reach
68
+ * for this directly.
69
+ */
70
+ export declare function resetTransportConfigCache(): void;
71
+ /**
72
+ * Parse an HTTP `Retry-After` header value into a delay in milliseconds.
73
+ *
74
+ * The header takes one of two forms (RFC 9110 § 10.2.3):
75
+ *
76
+ * - **delta-seconds** — an integer number of seconds (e.g. `Retry-After: 30`).
77
+ * - **HTTP-date** — an absolute timestamp (e.g.
78
+ * `Retry-After: Fri, 31 Dec 2025 23:59:59 GMT`); delay is computed as
79
+ * `target - now`, clamped to ≥ 0.
80
+ *
81
+ * Returns `undefined` when the header is absent, malformed, or yields a
82
+ * negative delay; callers fall back to computed exponential backoff.
83
+ *
84
+ * `nowMs` is injectable for deterministic tests.
85
+ */
86
+ export declare function parseRetryAfter(headerValue: string | undefined, nowMs?: number): number | undefined;
87
+ /**
88
+ * Compute exponential-backoff delay in milliseconds for a retry attempt.
89
+ *
90
+ * Formula: `baseMs × 2^attempt`, with ±25 % uniform jitter, clamped to
91
+ * `capMs`. `attempt` is zero-indexed (0 = first retry).
92
+ *
93
+ * 429 responses use a longer base (250 ms) than 5xx (100 ms) so a rate-limit
94
+ * recovery walks back further than a transient server hiccup. The jitter
95
+ * (`random` injectable for tests) reduces the thundering-herd risk when many
96
+ * MCP tool calls land on the same 429 window simultaneously.
97
+ */
98
+ export declare function computeBackoffDelay(reason: "rate-limit" | "server-error", attempt: number, random?: () => number): number;
99
+ /**
100
+ * Sleep for `ms` milliseconds, rejecting with the signal's abort reason if
101
+ * the caller aborts during the wait. Used for between-retry backoff so a
102
+ * cancellation propagates promptly rather than waiting out the full delay.
103
+ */
104
+ export declare function sleepUnlessAborted(ms: number, signal?: AbortSignal): Promise<void>;
105
+ /**
106
+ * Combine the caller's `AbortSignal` (if any) with a per-attempt internal
107
+ * timeout into a single signal usable by the underlying transport. Returns
108
+ * the combined signal plus a `dispose()` to release the timeout resource
109
+ * once the attempt completes (preventing dangling timers).
110
+ *
111
+ * When no caller signal is provided, the combined signal IS the timeout
112
+ * signal directly — no `AbortSignal.any` wrapping overhead.
113
+ */
114
+ export declare function combineSignals(callerSignal: AbortSignal | undefined, timeoutMs: number): {
115
+ signal: AbortSignal;
116
+ dispose: () => void;
117
+ };
118
+ /**
119
+ * Classify an error thrown by the underlying transport into a stable
120
+ * category so the retry loop can decide whether to retry, surface, or
121
+ * re-throw.
122
+ *
123
+ * - `aborted-by-caller` — the caller's signal aborted (cancellation).
124
+ * - `timeout` — the internal per-attempt timeout fired.
125
+ * - `network` — any other transport-level failure (DNS, socket
126
+ * reset, TLS error). NOT retried in v1 (could be a
127
+ * follow-up for #229 if empirical wedge data shows
128
+ * retries help).
129
+ */
130
+ export declare function classifyTransportError(err: unknown, callerSignal: AbortSignal | undefined): "aborted-by-caller" | "timeout" | "network";
131
+ /**
132
+ * Determine whether a response status code is retryable per the issue
133
+ * #229 policy.
134
+ */
135
+ export declare function isRetryableStatus(status: number): boolean;
136
+ //# sourceMappingURL=transport-resilience.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport-resilience.d.ts","sourceRoot":"","sources":["../src/transport-resilience.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,cAAe,SAAQ,UAAU;aAO1B,OAAO,EAAE,aAAa;aACtB,QAAQ,EAAE,MAAM;aAChB,QAAQ,EAAE,MAAM;aAEhB,UAAU,CAAC,EAAE,MAAM;aACnB,gBAAgB,CAAC,EAAE,MAAM;IAX3C,SAAkB,IAAI,oBAAoB;IAC1C,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,CAAC;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAGxB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,EAC7C,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChC,OAAO,EAAE,MAAM,EACC,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,gBAAgB,CAAC,EAAE,MAAM,YAAA,EACzC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAMhC;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;CACpB;AAUD,wBAAgB,mBAAmB,IAAI,eAAe,CAQrD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAkBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,GAAE,MAAmB,GAAG,MAAM,GAAG,SAAS,CAmB/G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,GAAG,cAAc,EACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAMR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlF;AAeD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,WAAW,GAAG,SAAS,EACrC,SAAS,EAAE,MAAM,GAChB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAe9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,EACZ,YAAY,EAAE,WAAW,GAAG,SAAS,GACpC,mBAAmB,GAAG,SAAS,GAAG,SAAS,CAO7C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD"}
@@ -0,0 +1,247 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { TtctlError } from "./auth/errors.js";
4
+ /**
5
+ * Final-failure transport error raised after all retries are exhausted or
6
+ * when a fatal signal-driven condition (caller abort, internal timeout)
7
+ * terminates the request loop.
8
+ *
9
+ * Extends {@link TtctlError} so it propagates through service callers'
10
+ * `if (err instanceof TtctlError) throw err;` guard verbatim — surfaces
11
+ * (CLI / MCP) render `error.recovery` directly.
12
+ *
13
+ * **Codes**:
14
+ *
15
+ * - `TIMEOUT` — internal per-request timeout fired (default 30s,
16
+ * overridable via `TTCTL_TRANSPORT_TIMEOUT_MS`). The
17
+ * underlying socket/handshake never completed in time.
18
+ * - `ABORTED` — caller's `AbortSignal` aborted before the request
19
+ * completed. Typically driven by the MCP client cancelling
20
+ * an in-flight tool call.
21
+ * - `RATE_LIMITED` — HTTP 429 returned by the surface and the retry budget
22
+ * was exhausted. `lastRetryAfterMs` (when set) reports
23
+ * the server's last `Retry-After` hint.
24
+ * - `SERVER_ERROR` — HTTP 5xx returned by the surface and the retry budget
25
+ * was exhausted.
26
+ */
27
+ export class TransportError extends TtctlError {
28
+ surface;
29
+ endpoint;
30
+ attempts;
31
+ lastStatus;
32
+ lastRetryAfterMs;
33
+ name = "TransportError";
34
+ code;
35
+ recovery;
36
+ constructor(code, surface, endpoint, attempts, message, lastStatus, lastRetryAfterMs, options) {
37
+ super(message, options);
38
+ this.surface = surface;
39
+ this.endpoint = endpoint;
40
+ this.attempts = attempts;
41
+ this.lastStatus = lastStatus;
42
+ this.lastRetryAfterMs = lastRetryAfterMs;
43
+ this.code = code;
44
+ this.recovery = RECOVERY[code];
45
+ }
46
+ }
47
+ const RECOVERY = {
48
+ TIMEOUT: "The Toptal API did not respond in time. Try again; if the wedge persists, file an issue.",
49
+ ABORTED: "Request was cancelled by the caller before it completed.",
50
+ RATE_LIMITED: "Toptal returned HTTP 429 (rate limited) after the configured retry budget. Wait a few minutes and retry.",
51
+ SERVER_ERROR: "Toptal returned a 5xx response after the configured retry budget. Retry later or file an issue.",
52
+ };
53
+ const DEFAULT_CONFIG = Object.freeze({
54
+ timeoutMs: 30_000,
55
+ connectMs: 10_000,
56
+ maxRetries: 3,
57
+ });
58
+ let cachedConfig;
59
+ export function readTransportConfig() {
60
+ if (cachedConfig !== undefined)
61
+ return cachedConfig;
62
+ cachedConfig = {
63
+ timeoutMs: readPositiveIntEnv("TTCTL_TRANSPORT_TIMEOUT_MS", DEFAULT_CONFIG.timeoutMs),
64
+ connectMs: readPositiveIntEnv("TTCTL_TRANSPORT_CONNECT_TIMEOUT_MS", DEFAULT_CONFIG.connectMs),
65
+ maxRetries: readBoundedIntEnv("TTCTL_TRANSPORT_MAX_RETRIES", DEFAULT_CONFIG.maxRetries, 0, 10),
66
+ };
67
+ return cachedConfig;
68
+ }
69
+ /**
70
+ * Reset the cached transport config so the next {@link readTransportConfig}
71
+ * call re-reads the environment. Test-only — production callers never reach
72
+ * for this directly.
73
+ */
74
+ export function resetTransportConfigCache() {
75
+ cachedConfig = undefined;
76
+ }
77
+ function readPositiveIntEnv(name, defaultValue) {
78
+ const raw = process.env[name];
79
+ if (raw === undefined || raw === "")
80
+ return defaultValue;
81
+ const parsed = Number.parseInt(raw, 10);
82
+ if (!Number.isFinite(parsed) || parsed < 1)
83
+ return defaultValue;
84
+ return parsed;
85
+ }
86
+ function readBoundedIntEnv(name, defaultValue, min, max) {
87
+ const raw = process.env[name];
88
+ if (raw === undefined || raw === "")
89
+ return defaultValue;
90
+ const parsed = Number.parseInt(raw, 10);
91
+ if (!Number.isFinite(parsed) || parsed < min || parsed > max)
92
+ return defaultValue;
93
+ return parsed;
94
+ }
95
+ /**
96
+ * Parse an HTTP `Retry-After` header value into a delay in milliseconds.
97
+ *
98
+ * The header takes one of two forms (RFC 9110 § 10.2.3):
99
+ *
100
+ * - **delta-seconds** — an integer number of seconds (e.g. `Retry-After: 30`).
101
+ * - **HTTP-date** — an absolute timestamp (e.g.
102
+ * `Retry-After: Fri, 31 Dec 2025 23:59:59 GMT`); delay is computed as
103
+ * `target - now`, clamped to ≥ 0.
104
+ *
105
+ * Returns `undefined` when the header is absent, malformed, or yields a
106
+ * negative delay; callers fall back to computed exponential backoff.
107
+ *
108
+ * `nowMs` is injectable for deterministic tests.
109
+ */
110
+ export function parseRetryAfter(headerValue, nowMs = Date.now()) {
111
+ if (headerValue === undefined)
112
+ return undefined;
113
+ const trimmed = headerValue.trim();
114
+ if (trimmed === "")
115
+ return undefined;
116
+ // delta-seconds — integer form first; tolerant of leading-zero / surrounding-space variants.
117
+ if (/^\d+$/.test(trimmed)) {
118
+ const seconds = Number.parseInt(trimmed, 10);
119
+ if (!Number.isFinite(seconds) || seconds < 0)
120
+ return undefined;
121
+ return seconds * 1000;
122
+ }
123
+ // HTTP-date form. RFC 9110 requires `IMF-fixdate` / `obs-date` shapes
124
+ // which always carry alphabetic day-name / month-name tokens; reject
125
+ // pure-numeric leftovers (e.g. `"-1"`, `"12.5"`) that `Date.parse`
126
+ // would otherwise opportunistically interpret as a year.
127
+ if (!/[A-Za-z]/.test(trimmed))
128
+ return undefined;
129
+ const parsed = Date.parse(trimmed);
130
+ if (Number.isNaN(parsed))
131
+ return undefined;
132
+ const delta = parsed - nowMs;
133
+ return delta > 0 ? delta : 0;
134
+ }
135
+ /**
136
+ * Compute exponential-backoff delay in milliseconds for a retry attempt.
137
+ *
138
+ * Formula: `baseMs × 2^attempt`, with ±25 % uniform jitter, clamped to
139
+ * `capMs`. `attempt` is zero-indexed (0 = first retry).
140
+ *
141
+ * 429 responses use a longer base (250 ms) than 5xx (100 ms) so a rate-limit
142
+ * recovery walks back further than a transient server hiccup. The jitter
143
+ * (`random` injectable for tests) reduces the thundering-herd risk when many
144
+ * MCP tool calls land on the same 429 window simultaneously.
145
+ */
146
+ export function computeBackoffDelay(reason, attempt, random = Math.random) {
147
+ const baseMs = reason === "rate-limit" ? 250 : 100;
148
+ const capMs = 30_000;
149
+ const raw = baseMs * 2 ** attempt;
150
+ const jitter = 1 + (random() * 2 - 1) * 0.25; // ±25%
151
+ return Math.min(Math.max(0, Math.round(raw * jitter)), capMs);
152
+ }
153
+ /**
154
+ * Sleep for `ms` milliseconds, rejecting with the signal's abort reason if
155
+ * the caller aborts during the wait. Used for between-retry backoff so a
156
+ * cancellation propagates promptly rather than waiting out the full delay.
157
+ */
158
+ export function sleepUnlessAborted(ms, signal) {
159
+ return new Promise((resolve, reject) => {
160
+ if (signal?.aborted) {
161
+ reject(toError(signal.reason));
162
+ return;
163
+ }
164
+ const timer = setTimeout(() => {
165
+ if (signal !== undefined) {
166
+ signal.removeEventListener("abort", onAbort);
167
+ }
168
+ resolve();
169
+ }, ms);
170
+ const onAbort = () => {
171
+ clearTimeout(timer);
172
+ // `signal` is guaranteed defined here — `onAbort` is registered only
173
+ // via the `signal?.addEventListener` branch below — but the listener
174
+ // closure references the outer-scope binding so eslint cannot infer
175
+ // narrowing across the boundary. Read defensively.
176
+ reject(toError(signal?.reason));
177
+ };
178
+ signal?.addEventListener("abort", onAbort, { once: true });
179
+ });
180
+ }
181
+ /**
182
+ * Normalise an arbitrary `AbortSignal.reason` value into an `Error`. The
183
+ * runtime convention is for `reason` to BE an `Error` (DOMException for
184
+ * `AbortSignal.timeout`, custom `Error` for `AbortController.abort(err)`),
185
+ * but the type is `unknown` and operators occasionally pass strings or
186
+ * `undefined`. Wrap non-Error shapes so downstream Promise consumers
187
+ * always see a real Error.
188
+ */
189
+ function toError(value) {
190
+ if (value instanceof Error)
191
+ return value;
192
+ return new Error(typeof value === "string" ? value : "Aborted");
193
+ }
194
+ /**
195
+ * Combine the caller's `AbortSignal` (if any) with a per-attempt internal
196
+ * timeout into a single signal usable by the underlying transport. Returns
197
+ * the combined signal plus a `dispose()` to release the timeout resource
198
+ * once the attempt completes (preventing dangling timers).
199
+ *
200
+ * When no caller signal is provided, the combined signal IS the timeout
201
+ * signal directly — no `AbortSignal.any` wrapping overhead.
202
+ */
203
+ export function combineSignals(callerSignal, timeoutMs) {
204
+ const timeoutController = new AbortController();
205
+ const timer = setTimeout(() => {
206
+ timeoutController.abort(new DOMException("Transport request timed out.", "TimeoutError"));
207
+ }, timeoutMs);
208
+ // Unref so a stray timer doesn't keep Node alive on early caller resolution.
209
+ timer.unref();
210
+ const signal = callerSignal === undefined ? timeoutController.signal : AbortSignal.any([callerSignal, timeoutController.signal]);
211
+ return {
212
+ signal,
213
+ dispose: () => {
214
+ clearTimeout(timer);
215
+ },
216
+ };
217
+ }
218
+ /**
219
+ * Classify an error thrown by the underlying transport into a stable
220
+ * category so the retry loop can decide whether to retry, surface, or
221
+ * re-throw.
222
+ *
223
+ * - `aborted-by-caller` — the caller's signal aborted (cancellation).
224
+ * - `timeout` — the internal per-attempt timeout fired.
225
+ * - `network` — any other transport-level failure (DNS, socket
226
+ * reset, TLS error). NOT retried in v1 (could be a
227
+ * follow-up for #229 if empirical wedge data shows
228
+ * retries help).
229
+ */
230
+ export function classifyTransportError(err, callerSignal) {
231
+ if (callerSignal?.aborted)
232
+ return "aborted-by-caller";
233
+ if (err instanceof DOMException && err.name === "TimeoutError")
234
+ return "timeout";
235
+ if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
236
+ return "timeout";
237
+ }
238
+ return "network";
239
+ }
240
+ /**
241
+ * Determine whether a response status code is retryable per the issue
242
+ * #229 policy.
243
+ */
244
+ export function isRetryableStatus(status) {
245
+ return status === 429 || (status >= 500 && status < 600);
246
+ }
247
+ //# sourceMappingURL=transport-resilience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport-resilience.js","sourceRoot":"","sources":["../src/transport-resilience.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,oCAAoC;AAEpC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,cAAe,SAAQ,UAAU;IAO1B;IACA;IACA;IAEA;IACA;IAXA,IAAI,GAAG,gBAAgB,CAAC;IACjC,IAAI,CAA0D;IAC9D,QAAQ,CAAS;IAE1B,YACE,IAA6D,EAC7C,OAAsB,EACtB,QAAgB,EAChB,QAAgB,EAChC,OAAe,EACC,UAAmB,EACnB,gBAAyB,EACzC,OAA6B;QAE7B,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QARR,YAAO,GAAP,OAAO,CAAe;QACtB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAS;QACnB,qBAAgB,GAAhB,gBAAgB,CAAS;QAIzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;CACF;AAED,MAAM,QAAQ,GAAqD;IACjE,OAAO,EAAE,0FAA0F;IACnG,OAAO,EAAE,0DAA0D;IACnE,YAAY,EACV,0GAA0G;IAC5G,YAAY,EAAE,iGAAiG;CAChH,CAAC;AA4BF,MAAM,cAAc,GAAoB,MAAM,CAAC,MAAM,CAAC;IACpD,SAAS,EAAE,MAAM;IACjB,SAAS,EAAE,MAAM;IACjB,UAAU,EAAE,CAAC;CACd,CAAC,CAAC;AAEH,IAAI,YAAyC,CAAC;AAE9C,MAAM,UAAU,mBAAmB;IACjC,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC;IACpD,YAAY,GAAG;QACb,SAAS,EAAE,kBAAkB,CAAC,4BAA4B,EAAE,cAAc,CAAC,SAAS,CAAC;QACrF,SAAS,EAAE,kBAAkB,CAAC,oCAAoC,EAAE,cAAc,CAAC,SAAS,CAAC;QAC7F,UAAU,EAAE,iBAAiB,CAAC,6BAA6B,EAAE,cAAc,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;KAC/F,CAAC;IACF,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB;IACvC,YAAY,GAAG,SAAS,CAAC;AAC3B,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY,EAAE,YAAoB;IAC5D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,YAAY,CAAC;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAChE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,YAAoB,EAAE,GAAW,EAAE,GAAW;IACrF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,YAAY,CAAC;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,YAAY,CAAC;IAClF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,WAA+B,EAAE,QAAgB,IAAI,CAAC,GAAG,EAAE;IACzF,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACrC,6FAA6F;IAC7F,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/D,OAAO,OAAO,GAAG,IAAI,CAAC;IACxB,CAAC;IACD,sEAAsE;IACtE,qEAAqE;IACrE,mEAAmE;IACnE,yDAAyD;IACzD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;IAC7B,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAqC,EACrC,OAAe,EACf,SAAuB,IAAI,CAAC,MAAM;IAElC,MAAM,MAAM,GAAG,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM,CAAC;IACrB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,MAAoB;IACjE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAC/B,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,qEAAqE;YACrE,qEAAqE;YACrE,oEAAoE;YACpE,mDAAmD;YACnD,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAClC,CAAC,CAAC;QACF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC;IACzC,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,YAAqC,EACrC,SAAiB;IAEjB,MAAM,iBAAiB,GAAG,IAAI,eAAe,EAAE,CAAC;IAChD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,iBAAiB,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC,CAAC;IAC5F,CAAC,EAAE,SAAS,CAAC,CAAC;IACd,6EAA6E;IAC7E,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,MAAM,MAAM,GACV,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;IACpH,OAAO;QACL,MAAM;QACN,OAAO,EAAE,GAAS,EAAE;YAClB,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAAY,EACZ,YAAqC;IAErC,IAAI,YAAY,EAAE,OAAO;QAAE,OAAO,mBAAmB,CAAC;IACtD,IAAI,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,SAAS,CAAC;IACjF,IAAI,GAAG,YAAY,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc,CAAC,EAAE,CAAC;QACvF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,CAAC;AAC3D,CAAC"}