msteams-mcp 0.2.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 (80) hide show
  1. package/README.md +229 -0
  2. package/dist/__fixtures__/api-responses.d.ts +228 -0
  3. package/dist/__fixtures__/api-responses.js +217 -0
  4. package/dist/api/chatsvc-api.d.ts +171 -0
  5. package/dist/api/chatsvc-api.js +459 -0
  6. package/dist/api/csa-api.d.ts +44 -0
  7. package/dist/api/csa-api.js +148 -0
  8. package/dist/api/index.d.ts +6 -0
  9. package/dist/api/index.js +6 -0
  10. package/dist/api/substrate-api.d.ts +50 -0
  11. package/dist/api/substrate-api.js +305 -0
  12. package/dist/auth/crypto.d.ts +32 -0
  13. package/dist/auth/crypto.js +66 -0
  14. package/dist/auth/index.d.ts +6 -0
  15. package/dist/auth/index.js +6 -0
  16. package/dist/auth/session-store.d.ts +82 -0
  17. package/dist/auth/session-store.js +136 -0
  18. package/dist/auth/token-extractor.d.ts +69 -0
  19. package/dist/auth/token-extractor.js +330 -0
  20. package/dist/browser/auth.d.ts +43 -0
  21. package/dist/browser/auth.js +232 -0
  22. package/dist/browser/context.d.ts +40 -0
  23. package/dist/browser/context.js +121 -0
  24. package/dist/browser/session.d.ts +34 -0
  25. package/dist/browser/session.js +92 -0
  26. package/dist/constants.d.ts +54 -0
  27. package/dist/constants.js +72 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +12 -0
  30. package/dist/research/explore.d.ts +11 -0
  31. package/dist/research/explore.js +267 -0
  32. package/dist/research/search-research.d.ts +17 -0
  33. package/dist/research/search-research.js +317 -0
  34. package/dist/server.d.ts +64 -0
  35. package/dist/server.js +291 -0
  36. package/dist/teams/api-interceptor.d.ts +54 -0
  37. package/dist/teams/api-interceptor.js +391 -0
  38. package/dist/teams/direct-api.d.ts +321 -0
  39. package/dist/teams/direct-api.js +1305 -0
  40. package/dist/teams/messages.d.ts +14 -0
  41. package/dist/teams/messages.js +142 -0
  42. package/dist/teams/search.d.ts +40 -0
  43. package/dist/teams/search.js +458 -0
  44. package/dist/test/cli.d.ts +12 -0
  45. package/dist/test/cli.js +328 -0
  46. package/dist/test/debug-search.d.ts +10 -0
  47. package/dist/test/debug-search.js +147 -0
  48. package/dist/test/manual-test.d.ts +11 -0
  49. package/dist/test/manual-test.js +160 -0
  50. package/dist/test/mcp-harness.d.ts +17 -0
  51. package/dist/test/mcp-harness.js +427 -0
  52. package/dist/tools/auth-tools.d.ts +26 -0
  53. package/dist/tools/auth-tools.js +127 -0
  54. package/dist/tools/index.d.ts +45 -0
  55. package/dist/tools/index.js +12 -0
  56. package/dist/tools/message-tools.d.ts +139 -0
  57. package/dist/tools/message-tools.js +433 -0
  58. package/dist/tools/people-tools.d.ts +46 -0
  59. package/dist/tools/people-tools.js +123 -0
  60. package/dist/tools/registry.d.ts +23 -0
  61. package/dist/tools/registry.js +61 -0
  62. package/dist/tools/search-tools.d.ts +79 -0
  63. package/dist/tools/search-tools.js +168 -0
  64. package/dist/types/errors.d.ts +58 -0
  65. package/dist/types/errors.js +132 -0
  66. package/dist/types/result.d.ts +43 -0
  67. package/dist/types/result.js +51 -0
  68. package/dist/types/teams.d.ts +79 -0
  69. package/dist/types/teams.js +5 -0
  70. package/dist/utils/api-config.d.ts +66 -0
  71. package/dist/utils/api-config.js +113 -0
  72. package/dist/utils/auth-guards.d.ts +29 -0
  73. package/dist/utils/auth-guards.js +54 -0
  74. package/dist/utils/http.d.ts +29 -0
  75. package/dist/utils/http.js +111 -0
  76. package/dist/utils/parsers.d.ts +187 -0
  77. package/dist/utils/parsers.js +574 -0
  78. package/dist/utils/parsers.test.d.ts +7 -0
  79. package/dist/utils/parsers.test.js +360 -0
  80. package/package.json +58 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Result type for operations that can fail.
3
+ *
4
+ * Provides a discriminated union for success/failure that
5
+ * forces callers to handle errors explicitly.
6
+ */
7
+ import type { McpError } from './errors.js';
8
+ /** Successful result with a value. */
9
+ export interface Ok<T> {
10
+ ok: true;
11
+ value: T;
12
+ }
13
+ /** Failed result with an error. */
14
+ export interface Err<E = McpError> {
15
+ ok: false;
16
+ error: E;
17
+ }
18
+ /** A result that can be either a success or failure. */
19
+ export type Result<T, E = McpError> = Ok<T> | Err<E>;
20
+ /**
21
+ * Creates a successful result.
22
+ */
23
+ export declare function ok<T>(value: T): Ok<T>;
24
+ /**
25
+ * Creates a failed result.
26
+ */
27
+ export declare function err<E = McpError>(error: E): Err<E>;
28
+ /**
29
+ * Unwraps a result, throwing if it's an error.
30
+ */
31
+ export declare function unwrap<T>(result: Result<T>): T;
32
+ /**
33
+ * Unwraps a result with a default value for errors.
34
+ */
35
+ export declare function unwrapOr<T>(result: Result<T>, defaultValue: T): T;
36
+ /**
37
+ * Maps a successful result to a new value.
38
+ */
39
+ export declare function map<T, U>(result: Result<T>, fn: (value: T) => U): Result<U>;
40
+ /**
41
+ * Maps a successful result to a new result (flatMap).
42
+ */
43
+ export declare function andThen<T, U>(result: Result<T>, fn: (value: T) => Result<U>): Result<U>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Result type for operations that can fail.
3
+ *
4
+ * Provides a discriminated union for success/failure that
5
+ * forces callers to handle errors explicitly.
6
+ */
7
+ /**
8
+ * Creates a successful result.
9
+ */
10
+ export function ok(value) {
11
+ return { ok: true, value };
12
+ }
13
+ /**
14
+ * Creates a failed result.
15
+ */
16
+ export function err(error) {
17
+ return { ok: false, error };
18
+ }
19
+ /**
20
+ * Unwraps a result, throwing if it's an error.
21
+ */
22
+ export function unwrap(result) {
23
+ if (result.ok) {
24
+ return result.value;
25
+ }
26
+ throw new Error(result.error.message);
27
+ }
28
+ /**
29
+ * Unwraps a result with a default value for errors.
30
+ */
31
+ export function unwrapOr(result, defaultValue) {
32
+ return result.ok ? result.value : defaultValue;
33
+ }
34
+ /**
35
+ * Maps a successful result to a new value.
36
+ */
37
+ export function map(result, fn) {
38
+ if (result.ok) {
39
+ return ok(fn(result.value));
40
+ }
41
+ return result;
42
+ }
43
+ /**
44
+ * Maps a successful result to a new result (flatMap).
45
+ */
46
+ export function andThen(result, fn) {
47
+ if (result.ok) {
48
+ return fn(result.value);
49
+ }
50
+ return result;
51
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * TypeScript interfaces for Teams data structures.
3
+ * These will be refined based on research findings.
4
+ */
5
+ export interface TeamsSearchResult {
6
+ id: string;
7
+ type: 'message' | 'file' | 'person';
8
+ content: string;
9
+ sender?: string;
10
+ timestamp?: string;
11
+ channelName?: string;
12
+ teamName?: string;
13
+ conversationId?: string;
14
+ messageId?: string;
15
+ /** Direct link to open this message in Teams */
16
+ messageLink?: string;
17
+ }
18
+ export interface TeamsMessage {
19
+ id: string;
20
+ content: string;
21
+ sender: string;
22
+ timestamp: string;
23
+ channelName?: string;
24
+ teamName?: string;
25
+ replyCount?: number;
26
+ reactions?: TeamsReaction[];
27
+ }
28
+ export interface TeamsReaction {
29
+ type: string;
30
+ count: number;
31
+ }
32
+ export interface InterceptedRequest {
33
+ url: string;
34
+ method: string;
35
+ headers: Record<string, string>;
36
+ postData?: string;
37
+ timestamp: Date;
38
+ }
39
+ export interface InterceptedResponse {
40
+ url: string;
41
+ status: number;
42
+ headers: Record<string, string>;
43
+ body?: string;
44
+ timestamp: Date;
45
+ }
46
+ export interface SearchApiEndpoint {
47
+ url: string;
48
+ method: string;
49
+ description: string;
50
+ requestFormat?: unknown;
51
+ responseFormat?: unknown;
52
+ }
53
+ /** Pagination options for search requests. */
54
+ export interface SearchPaginationOptions {
55
+ /** Starting offset (0-based). Default: 0 */
56
+ from?: number;
57
+ /** Page size. Default: 25 */
58
+ size?: number;
59
+ /** Maximum total results to fetch across all pages. */
60
+ maxResults?: number;
61
+ }
62
+ /** Pagination metadata returned with search results. */
63
+ export interface SearchPaginationResult {
64
+ /** Number of results returned in this response. */
65
+ returned: number;
66
+ /** Starting offset used for this request. */
67
+ from: number;
68
+ /** Page size requested. */
69
+ size: number;
70
+ /** Total results available (if known). */
71
+ total?: number;
72
+ /** Whether more results are available. */
73
+ hasMore: boolean;
74
+ }
75
+ /** Search results with pagination metadata. */
76
+ export interface TeamsSearchResultsWithPagination {
77
+ results: TeamsSearchResult[];
78
+ pagination: SearchPaginationResult;
79
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * TypeScript interfaces for Teams data structures.
3
+ * These will be refined based on research findings.
4
+ */
5
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * API endpoint configuration and header utilities.
3
+ *
4
+ * Centralises all API URLs and common request headers.
5
+ */
6
+ /** Valid API regions. */
7
+ export declare const VALID_REGIONS: readonly ["amer", "emea", "apac"];
8
+ export type Region = typeof VALID_REGIONS[number];
9
+ /**
10
+ * Validates a region string.
11
+ * @throws Error if region is invalid.
12
+ */
13
+ export declare function validateRegion(region: string): Region;
14
+ /**
15
+ * Attempts to parse region from a URL or returns default.
16
+ */
17
+ export declare function parseRegionFromUrl(url: string): Region;
18
+ /** Substrate API endpoints. */
19
+ export declare const SUBSTRATE_API: {
20
+ /** Full-text message search. */
21
+ readonly search: "https://substrate.office.com/searchservice/api/v2/query";
22
+ /** People and message typeahead suggestions. */
23
+ readonly suggestions: "https://substrate.office.com/search/api/v1/suggestions";
24
+ /** Frequent contacts list. */
25
+ readonly frequentContacts: "https://substrate.office.com/search/api/v1/suggestions?scenario=peoplecache";
26
+ /** People search. */
27
+ readonly peopleSearch: "https://substrate.office.com/search/api/v1/suggestions?scenario=powerbar";
28
+ /** Channel search (org-wide, not just user's teams). */
29
+ readonly channelSearch: "https://substrate.office.com/search/api/v1/suggestions?scenario=powerbar&setflight=TurnOffMPLSuppressionTeams,EnableTeamsChannelDomainPowerbar&domain=TeamsChannel";
30
+ };
31
+ /** Chat service API endpoints. */
32
+ export declare const CHATSVC_API: {
33
+ /**
34
+ * Get messages URL for a conversation.
35
+ *
36
+ * For thread replies in channels, provide replyToMessageId to append
37
+ * `;messageid={id}` to the conversation path. This tells Teams the message
38
+ * is a reply to an existing thread rather than a new top-level post.
39
+ */
40
+ readonly messages: (region: Region, conversationId: string, replyToMessageId?: string) => string;
41
+ /** Get conversation metadata URL. */
42
+ readonly conversation: (region: Region, conversationId: string) => string;
43
+ /** Save/unsave message metadata URL. */
44
+ readonly messageMetadata: (region: Region, conversationId: string, messageId: string) => string;
45
+ /** Edit a specific message URL. */
46
+ readonly editMessage: (region: Region, conversationId: string, messageId: string) => string;
47
+ /** Delete a specific message URL (soft delete). */
48
+ readonly deleteMessage: (region: Region, conversationId: string, messageId: string) => string;
49
+ };
50
+ /** CSA (Chat Service Aggregator) API endpoints. */
51
+ export declare const CSA_API: {
52
+ /** Conversation folders (favourites) URL. */
53
+ readonly conversationFolders: (region: Region) => string;
54
+ /** Teams list (all teams/channels user is a member of). */
55
+ readonly teamsList: (region: Region) => string;
56
+ };
57
+ /** Common request headers for Teams API calls. */
58
+ export declare function getTeamsHeaders(): HeadersInit;
59
+ /** Headers for Bearer token authentication. */
60
+ export declare function getBearerHeaders(token: string): HeadersInit;
61
+ /** Headers for Skype token + Bearer authentication. */
62
+ export declare function getSkypeAuthHeaders(skypeToken: string, authToken: string): HeadersInit;
63
+ /** Headers for CSA API (Skype token + CSA Bearer). */
64
+ export declare function getCsaHeaders(skypeToken: string, csaToken: string): HeadersInit;
65
+ /** Client version header for messaging API. */
66
+ export declare function getMessagingHeaders(skypeToken: string, authToken: string): HeadersInit;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * API endpoint configuration and header utilities.
3
+ *
4
+ * Centralises all API URLs and common request headers.
5
+ */
6
+ /** Valid API regions. */
7
+ export const VALID_REGIONS = ['amer', 'emea', 'apac'];
8
+ /**
9
+ * Validates a region string.
10
+ * @throws Error if region is invalid.
11
+ */
12
+ export function validateRegion(region) {
13
+ if (!VALID_REGIONS.includes(region)) {
14
+ throw new Error(`Invalid region: ${region}. Valid regions: ${VALID_REGIONS.join(', ')}`);
15
+ }
16
+ return region;
17
+ }
18
+ /**
19
+ * Attempts to parse region from a URL or returns default.
20
+ */
21
+ export function parseRegionFromUrl(url) {
22
+ for (const region of VALID_REGIONS) {
23
+ if (url.includes(`/api/chatsvc/${region}/`) ||
24
+ url.includes(`/api/csa/${region}/`)) {
25
+ return region;
26
+ }
27
+ }
28
+ return 'amer'; // Default region
29
+ }
30
+ /** Substrate API endpoints. */
31
+ export const SUBSTRATE_API = {
32
+ /** Full-text message search. */
33
+ search: 'https://substrate.office.com/searchservice/api/v2/query',
34
+ /** People and message typeahead suggestions. */
35
+ suggestions: 'https://substrate.office.com/search/api/v1/suggestions',
36
+ /** Frequent contacts list. */
37
+ frequentContacts: 'https://substrate.office.com/search/api/v1/suggestions?scenario=peoplecache',
38
+ /** People search. */
39
+ peopleSearch: 'https://substrate.office.com/search/api/v1/suggestions?scenario=powerbar',
40
+ /** Channel search (org-wide, not just user's teams). */
41
+ channelSearch: 'https://substrate.office.com/search/api/v1/suggestions?scenario=powerbar&setflight=TurnOffMPLSuppressionTeams,EnableTeamsChannelDomainPowerbar&domain=TeamsChannel',
42
+ };
43
+ /** Chat service API endpoints. */
44
+ export const CHATSVC_API = {
45
+ /**
46
+ * Get messages URL for a conversation.
47
+ *
48
+ * For thread replies in channels, provide replyToMessageId to append
49
+ * `;messageid={id}` to the conversation path. This tells Teams the message
50
+ * is a reply to an existing thread rather than a new top-level post.
51
+ */
52
+ messages: (region, conversationId, replyToMessageId) => {
53
+ // When replying to a thread, the URL includes ;messageid={threadRootId}
54
+ const conversationPath = replyToMessageId
55
+ ? `${conversationId};messageid=${replyToMessageId}`
56
+ : conversationId;
57
+ return `https://teams.microsoft.com/api/chatsvc/${region}/v1/users/ME/conversations/${encodeURIComponent(conversationPath)}/messages`;
58
+ },
59
+ /** Get conversation metadata URL. */
60
+ conversation: (region, conversationId) => `https://teams.microsoft.com/api/chatsvc/${region}/v1/users/ME/conversations/${encodeURIComponent(conversationId)}`,
61
+ /** Save/unsave message metadata URL. */
62
+ messageMetadata: (region, conversationId, messageId) => `https://teams.microsoft.com/api/chatsvc/${region}/v1/users/ME/conversations/${encodeURIComponent(conversationId)}/rcmetadata/${messageId}`,
63
+ /** Edit a specific message URL. */
64
+ editMessage: (region, conversationId, messageId) => `https://teams.microsoft.com/api/chatsvc/${region}/v1/users/ME/conversations/${encodeURIComponent(conversationId)}/messages/${messageId}`,
65
+ /** Delete a specific message URL (soft delete). */
66
+ deleteMessage: (region, conversationId, messageId) => `https://teams.microsoft.com/api/chatsvc/${region}/v1/users/ME/conversations/${encodeURIComponent(conversationId)}/messages/${messageId}?behavior=softDelete`,
67
+ };
68
+ /** CSA (Chat Service Aggregator) API endpoints. */
69
+ export const CSA_API = {
70
+ /** Conversation folders (favourites) URL. */
71
+ conversationFolders: (region) => `https://teams.microsoft.com/api/csa/${region}/api/v1/teams/users/me/conversationFolders?supportsAdditionalSystemGeneratedFolders=true&supportsSliceItems=true`,
72
+ /** Teams list (all teams/channels user is a member of). */
73
+ teamsList: (region) => `https://teams.microsoft.com/api/csa/${region}/api/v3/teams/users/me?isPrefetch=false&enableMembershipSummary=true`,
74
+ };
75
+ /** Common request headers for Teams API calls. */
76
+ export function getTeamsHeaders() {
77
+ return {
78
+ 'Content-Type': 'application/json',
79
+ 'Accept': 'application/json',
80
+ 'Origin': 'https://teams.microsoft.com',
81
+ 'Referer': 'https://teams.microsoft.com/',
82
+ };
83
+ }
84
+ /** Headers for Bearer token authentication. */
85
+ export function getBearerHeaders(token) {
86
+ return {
87
+ ...getTeamsHeaders(),
88
+ 'Authorization': `Bearer ${token}`,
89
+ };
90
+ }
91
+ /** Headers for Skype token + Bearer authentication. */
92
+ export function getSkypeAuthHeaders(skypeToken, authToken) {
93
+ return {
94
+ ...getTeamsHeaders(),
95
+ 'Authentication': `skypetoken=${skypeToken}`,
96
+ 'Authorization': `Bearer ${authToken}`,
97
+ };
98
+ }
99
+ /** Headers for CSA API (Skype token + CSA Bearer). */
100
+ export function getCsaHeaders(skypeToken, csaToken) {
101
+ return {
102
+ ...getTeamsHeaders(),
103
+ 'Authentication': `skypetoken=${skypeToken}`,
104
+ 'Authorization': `Bearer ${csaToken}`,
105
+ };
106
+ }
107
+ /** Client version header for messaging API. */
108
+ export function getMessagingHeaders(skypeToken, authToken) {
109
+ return {
110
+ ...getSkypeAuthHeaders(skypeToken, authToken),
111
+ 'X-Ms-Client-Version': '1415/1.0.0.2025010401',
112
+ };
113
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Authentication guard utilities.
3
+ *
4
+ * Provides reusable auth checks that return Result types for consistent
5
+ * error handling across API modules.
6
+ */
7
+ import { type McpError } from '../types/errors.js';
8
+ import { type Result } from '../types/result.js';
9
+ import { type MessageAuthInfo } from '../auth/token-extractor.js';
10
+ /** Authentication info for messaging and CSA APIs. */
11
+ export interface CsaAuthInfo {
12
+ auth: MessageAuthInfo;
13
+ csaToken: string;
14
+ }
15
+ /**
16
+ * Requires a valid Substrate token.
17
+ * Use for search and people APIs.
18
+ */
19
+ export declare function requireSubstrateToken(): Result<string, McpError>;
20
+ /**
21
+ * Requires valid message authentication.
22
+ * Use for chatsvc messaging APIs.
23
+ */
24
+ export declare function requireMessageAuth(): Result<MessageAuthInfo, McpError>;
25
+ /**
26
+ * Requires valid CSA authentication (message auth + CSA token).
27
+ * Use for favourites and team list APIs.
28
+ */
29
+ export declare function requireCsaAuth(): Result<CsaAuthInfo, McpError>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Authentication guard utilities.
3
+ *
4
+ * Provides reusable auth checks that return Result types for consistent
5
+ * error handling across API modules.
6
+ */
7
+ import { ErrorCode, createError } from '../types/errors.js';
8
+ import { err, ok } from '../types/result.js';
9
+ import { getValidSubstrateToken, extractMessageAuth, extractCsaToken, } from '../auth/token-extractor.js';
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Error Messages
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ const AUTH_ERROR_MESSAGES = {
14
+ substrateToken: 'No valid token available. Browser login required.',
15
+ messageAuth: 'No valid authentication. Browser login required.',
16
+ csaToken: 'No valid authentication for favourites. Browser login required.',
17
+ };
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // Guard Functions
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ /**
22
+ * Requires a valid Substrate token.
23
+ * Use for search and people APIs.
24
+ */
25
+ export function requireSubstrateToken() {
26
+ const token = getValidSubstrateToken();
27
+ if (!token) {
28
+ return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.substrateToken));
29
+ }
30
+ return ok(token);
31
+ }
32
+ /**
33
+ * Requires valid message authentication.
34
+ * Use for chatsvc messaging APIs.
35
+ */
36
+ export function requireMessageAuth() {
37
+ const auth = extractMessageAuth();
38
+ if (!auth) {
39
+ return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.messageAuth));
40
+ }
41
+ return ok(auth);
42
+ }
43
+ /**
44
+ * Requires valid CSA authentication (message auth + CSA token).
45
+ * Use for favourites and team list APIs.
46
+ */
47
+ export function requireCsaAuth() {
48
+ const auth = extractMessageAuth();
49
+ const csaToken = extractCsaToken();
50
+ if (!auth?.skypeToken || !csaToken) {
51
+ return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.csaToken));
52
+ }
53
+ return ok({ auth, csaToken });
54
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * HTTP utilities with retry, timeout, and error handling.
3
+ */
4
+ import { type Result } from '../types/result.js';
5
+ /** Options for HTTP requests. */
6
+ export interface HttpOptions extends Omit<RequestInit, 'signal'> {
7
+ /** Timeout in milliseconds (default: 30000). */
8
+ timeoutMs?: number;
9
+ /** Maximum retry attempts (default: 3). */
10
+ maxRetries?: number;
11
+ /** Base delay for exponential backoff in ms (default: 1000). */
12
+ retryBaseDelayMs?: number;
13
+ /** Maximum delay between retries in ms (default: 10000). */
14
+ retryMaxDelayMs?: number;
15
+ }
16
+ /** Successful HTTP response. */
17
+ export interface HttpResponse<T = unknown> {
18
+ status: number;
19
+ headers: Headers;
20
+ data: T;
21
+ }
22
+ /**
23
+ * Makes an HTTP request with timeout, retry, and error handling.
24
+ */
25
+ export declare function httpRequest<T = unknown>(url: string, options?: HttpOptions): Promise<Result<HttpResponse<T>>>;
26
+ /**
27
+ * Clears the rate limit state (for testing).
28
+ */
29
+ export declare function clearRateLimitState(): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * HTTP utilities with retry, timeout, and error handling.
3
+ */
4
+ import { ErrorCode, createError, classifyHttpError, extractRetryAfter, } from '../types/errors.js';
5
+ import { ok, err } from '../types/result.js';
6
+ /** Rate limit state tracking. */
7
+ let rateLimitedUntil = null;
8
+ /**
9
+ * Makes an HTTP request with timeout, retry, and error handling.
10
+ */
11
+ export async function httpRequest(url, options = {}) {
12
+ const { timeoutMs = 30000, maxRetries = 3, retryBaseDelayMs = 1000, retryMaxDelayMs = 10000, ...fetchOptions } = options;
13
+ // Check rate limit state
14
+ if (rateLimitedUntil && Date.now() < rateLimitedUntil) {
15
+ const waitMs = rateLimitedUntil - Date.now();
16
+ return err(createError(ErrorCode.RATE_LIMITED, `Rate limited. Retry after ${Math.ceil(waitMs / 1000)}s`, { retryable: true, retryAfterMs: waitMs }));
17
+ }
18
+ let lastError = null;
19
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
20
+ try {
21
+ const result = await fetchWithTimeout(url, fetchOptions, timeoutMs);
22
+ if (result.ok) {
23
+ return result;
24
+ }
25
+ lastError = result.error;
26
+ // Handle rate limiting
27
+ if (result.error.code === ErrorCode.RATE_LIMITED && result.error.retryAfterMs) {
28
+ rateLimitedUntil = Date.now() + result.error.retryAfterMs;
29
+ }
30
+ // Don't retry non-retryable errors
31
+ if (!result.error.retryable) {
32
+ return result;
33
+ }
34
+ // Don't retry on last attempt
35
+ if (attempt === maxRetries) {
36
+ return result;
37
+ }
38
+ // Calculate backoff delay
39
+ const delay = Math.min(retryBaseDelayMs * Math.pow(2, attempt - 1), retryMaxDelayMs);
40
+ await sleep(delay);
41
+ }
42
+ catch (error) {
43
+ // Unexpected errors
44
+ lastError = createError(ErrorCode.UNKNOWN, error instanceof Error ? error.message : String(error), { retryable: false });
45
+ if (attempt === maxRetries) {
46
+ return err(lastError);
47
+ }
48
+ }
49
+ }
50
+ return err(lastError ?? createError(ErrorCode.UNKNOWN, 'Request failed'));
51
+ }
52
+ /**
53
+ * Makes a single fetch request with timeout.
54
+ */
55
+ async function fetchWithTimeout(url, options, timeoutMs) {
56
+ const controller = new AbortController();
57
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
58
+ try {
59
+ const response = await fetch(url, {
60
+ ...options,
61
+ signal: controller.signal,
62
+ });
63
+ clearTimeout(timeoutId);
64
+ // Handle error responses
65
+ if (!response.ok) {
66
+ const retryAfterMs = extractRetryAfter(response.headers);
67
+ const errorText = await response.text().catch(() => '');
68
+ return err(createError(classifyHttpError(response.status, errorText), `HTTP ${response.status}: ${errorText || response.statusText}`, { retryAfterMs }));
69
+ }
70
+ // Parse JSON response
71
+ const contentType = response.headers.get('content-type') || '';
72
+ let data;
73
+ if (contentType.includes('application/json')) {
74
+ data = await response.json();
75
+ }
76
+ else {
77
+ data = await response.text();
78
+ }
79
+ return ok({
80
+ status: response.status,
81
+ headers: response.headers,
82
+ data,
83
+ });
84
+ }
85
+ catch (error) {
86
+ clearTimeout(timeoutId);
87
+ if (error instanceof Error) {
88
+ if (error.name === 'AbortError') {
89
+ return err(createError(ErrorCode.TIMEOUT, `Request timed out after ${timeoutMs}ms`, { retryable: true }));
90
+ }
91
+ if (error.message.includes('ECONNRESET') ||
92
+ error.message.includes('ETIMEDOUT') ||
93
+ error.message.includes('ENOTFOUND')) {
94
+ return err(createError(ErrorCode.NETWORK_ERROR, error.message, { retryable: true }));
95
+ }
96
+ }
97
+ return err(createError(ErrorCode.UNKNOWN, error instanceof Error ? error.message : String(error), { retryable: false }));
98
+ }
99
+ }
100
+ /**
101
+ * Sleep for a specified duration.
102
+ */
103
+ function sleep(ms) {
104
+ return new Promise(resolve => setTimeout(resolve, ms));
105
+ }
106
+ /**
107
+ * Clears the rate limit state (for testing).
108
+ */
109
+ export function clearRateLimitState() {
110
+ rateLimitedUntil = null;
111
+ }