@microsoft/webiq 0.0.1-alpha → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,423 @@
1
- # @microsoft/webiq
1
+ # Web IQ TypeScript SDK
2
2
 
3
- Coming soon.
3
+ Official TypeScript SDK for Web IQ APIs.
4
+
5
+ [![Node.js 22+](https://img.shields.io/badge/node-22+-green.svg)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license)
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @microsoft/webiq
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```typescript
17
+ import { WebIQClient, BrowseContentFormat, ContentFormat } from '@microsoft/webiq';
18
+
19
+ const client = new WebIQClient({
20
+ apiKey: process.env.WEBIQ_API_KEY,
21
+ });
22
+
23
+ // Web search with content format
24
+ const web = await client.web.search('TypeScript programming', {
25
+ maxResults: 5,
26
+ contentFormat: ContentFormat.HTML, // Use enum, not the string "html"
27
+ });
28
+ for (const result of web.webResults ?? []) {
29
+ console.log(`${result.title}: ${result.url}`);
30
+ }
31
+
32
+ // News search
33
+ const news = await client.news.search('technology', { maxResults: 5 });
34
+ for (const item of news.newsResults ?? []) {
35
+ console.log(`${item.title} - ${item.source}`);
36
+ }
37
+
38
+ // Video search
39
+ const videos = await client.videos.search('machine learning tutorial');
40
+ for (const video of videos.videoResults ?? []) {
41
+ console.log(`${video.title} (${video.length})`);
42
+ }
43
+
44
+ // Browse a URL in markdown format
45
+ const page = await client.browse.fetch('https://www.microsoft.com', {
46
+ contentFormat: BrowseContentFormat.MARKDOWN,
47
+ });
48
+ console.log(page.content);
49
+
50
+ // Classic search with multiple answer types
51
+ const classic = await client.classic.search('Artificial intelligence trends', {
52
+ responseFilter: ['webResults'],
53
+ });
54
+ // responseFilter selects which answer types come back; the matching
55
+ // fields (e.g. classic.webResults) are populated dynamically and typed as `any`.
56
+ for (const r of classic.webResults ?? []) {
57
+ console.log(` - ${r.title}: ${r.url}`);
58
+ }
59
+
60
+ await client.close();
61
+ ```
62
+
63
+ ## Authentication
64
+
65
+ ```typescript
66
+ import { WebIQClient } from '@microsoft/webiq';
67
+
68
+ const client = new WebIQClient({
69
+ apiKey: process.env.WEBIQ_API_KEY,
70
+ });
71
+ ```
72
+
73
+ ### EntraID (Azure AD)
74
+
75
+ Pass any `@azure/identity` `TokenCredential`:
76
+
77
+ ```typescript
78
+ import { DefaultAzureCredential } from '@azure/identity';
79
+ import { WebIQClient } from '@microsoft/webiq';
80
+
81
+ const client = new WebIQClient({
82
+ credential: new DefaultAzureCredential(),
83
+ });
84
+ ```
85
+
86
+ ## API Reference
87
+
88
+ ### Web Search
89
+
90
+ ```typescript
91
+ const results = await client.web.search(query, {
92
+ maxResults: 10, // 1-50, default 10
93
+ language: 'en', // ISO 639-1 code
94
+ region: 'US', // Country/region code
95
+ location: 'lat:40.7;long:-74.0', // Optional
96
+ contentFormat: ContentFormat.HTML, // PASSAGE, TEXT, HTML, MARKDOWN
97
+ maxLength: 10000, // 1-500000
98
+ });
99
+ // results.webResults → [{title, url, content, lastUpdatedAt, crawledAt, language, isAdult}]
100
+ ```
101
+
102
+ ### News Search
103
+
104
+ ```typescript
105
+ const results = await client.news.search(query, {
106
+ maxResults: 10, // 1-20, default 10
107
+ language: 'en',
108
+ region: 'US',
109
+ location: 'lat:40.7;long:-74.0', // Optional
110
+ contentFormat: ContentFormat.TEXT,
111
+ maxLength: 10000,
112
+ });
113
+ // results.newsResults → [{title, url, content, snippet, source, lastUpdatedAt, thumbnail, isAdult}]
114
+ ```
115
+
116
+ ### Video Search
117
+
118
+ ```typescript
119
+ const results = await client.videos.search(query, {
120
+ maxResults: 30, // 1-30, default 30
121
+ language: 'en',
122
+ region: 'US',
123
+ enablePlaylist: true,
124
+ freshness: 'month', // week, month, year
125
+ });
126
+ // results.videoResults → [{title, url, length, viewCount, publishedBy, moments, ...}]
127
+ // results.playlists → [{title, videos}]
128
+ ```
129
+
130
+ ### Browse
131
+
132
+ ```typescript
133
+ const page = await client.browse.fetch(url, {
134
+ maxLength: 10000, // 1-500000
135
+ liveCrawl: 'fallback', // "none" (default) | "fallback" | "force"
136
+ includeWebLinks: true,
137
+ includeImageLinks: true,
138
+ renderDynamicPages: false,
139
+ contentFormat: BrowseContentFormat.MARKDOWN,
140
+ });
141
+ // page → {url, title, content, isAdult, retryAfter, traceId, ...}
142
+ ```
143
+
144
+ ### Image Search
145
+
146
+ ```typescript
147
+ import { ImageAspectRatio, ImageSize, SafeSearch } from '@microsoft/webiq';
148
+
149
+ const results = await client.images.search(query, {
150
+ maxResults: 30, // 1-30, default 30
151
+ language: 'en',
152
+ region: 'US',
153
+ aspectRatio: ImageAspectRatio.WIDE, // SQUARE, WIDE, TALL
154
+ imageSize: ImageSize.LARGE, // SMALL, MEDIUM, LARGE, EXTRA_LARGE
155
+ safeSearch: SafeSearch.STRICT, // OFF, STRICT
156
+ watermarkFree: true,
157
+ });
158
+ // results.imageResults → [{title, url, hostPageUrl, caption, width, height, thumbnailUrl, ...}]
159
+ ```
160
+
161
+ ### Classic Search
162
+
163
+ Given a query, classic search will search and retrieve content from webpages, images, videos, news, weather, sports, etc.
164
+
165
+ ```typescript
166
+ import { ContentFormat, SafeSearchMode } from '@microsoft/webiq';
167
+
168
+ const response = await client.classic.search(query, {
169
+ maxAnswerTypes: 6, // 1-6, default 6
170
+ language: 'en', // ISO 639-1/2 or BCP 47
171
+ region: 'US', // Country/region code
172
+ location: 'lat:40.7;long:-74.0', // Optional
173
+ maxResultsWeb: 10, // 1-50, default 10
174
+ maxLength: 10000, // 1-500000
175
+ contentFormat: ContentFormat.HTML,
176
+ freshness: 'month', // day, week, month, year, or date range
177
+ responseFilter: ['webResults', 'newsResults'], // Answer types to include
178
+ safeSearch: SafeSearchMode.MODERATE, // OFF, MODERATE, STRICT
179
+ });
180
+ // response.querySignals → {originalQuery, normalizedQuery, isDefensive, isAdult, isNav, isFresh}
181
+ // response.traceId → string
182
+ // Additional dynamic fields for different answer types
183
+ ```
184
+
185
+ ### Enum Types
186
+
187
+ Some parameters require enum values instead of plain strings. Import them from `@microsoft/webiq`:
188
+
189
+ ```typescript
190
+ import { BrowseContentFormat, ContentFormat } from '@microsoft/webiq';
191
+
192
+ // ContentFormat: format of returned content for web, news, and classic search
193
+ ContentFormat.PASSAGE; // Selected passages only (plain text)
194
+ ContentFormat.TEXT; // Full page text (plain text)
195
+ ContentFormat.HTML; // HTML format (default for web search)
196
+ ContentFormat.MARKDOWN; // Markdown format
197
+
198
+ // BrowseContentFormat: format for browse (no PASSAGE option)
199
+ BrowseContentFormat.TEXT; // Full page text
200
+ BrowseContentFormat.HTML; // HTML format (default)
201
+ BrowseContentFormat.MARKDOWN; // Markdown format
202
+
203
+ // Usage in web search
204
+ const results = await client.web.search('query', {
205
+ contentFormat: ContentFormat.MARKDOWN,
206
+ });
207
+
208
+ // Usage in browse
209
+ const page = await client.browse.fetch('https://www.microsoft.com', {
210
+ contentFormat: BrowseContentFormat.HTML,
211
+ });
212
+ ```
213
+
214
+ ## Configuration
215
+
216
+ ### Timeout and Retry
217
+
218
+ ```typescript
219
+ import { WebIQClient, RetryPolicy } from '@microsoft/webiq';
220
+
221
+ const client = new WebIQClient({
222
+ apiKey: 'your-api-key',
223
+ timeout: 10000, // milliseconds (default: 10000)
224
+ retry: new RetryPolicy({
225
+ maxRetries: 2, // defaults shown
226
+ baseDelaySeconds: 0.25,
227
+ maxDelaySeconds: 4.0,
228
+ }),
229
+ });
230
+ ```
231
+
232
+ ### Cancellation
233
+
234
+ Every search and browse method accepts an `AbortSignal` via `options.signal`.
235
+ When the signal aborts, the in-flight request is cancelled, retries stop, and
236
+ the signal's `reason` is re-thrown unchanged (no `APIConnectionError` wrapping).
237
+
238
+ Propagate a client disconnect from an HTTP handler:
239
+
240
+ ```typescript
241
+ import express from 'express';
242
+
243
+ app.get('/search', async (req, res) => {
244
+ try {
245
+ const result = await client.web.search(req.query.q as string, {
246
+ signal: req.signal, // express/fastify expose the request's AbortSignal
247
+ });
248
+ res.json(result);
249
+ } catch (err) {
250
+ if (err instanceof DOMException && err.name === 'AbortError') return; // client gone
251
+ throw err;
252
+ }
253
+ });
254
+ ```
255
+
256
+ Apply a wall-clock budget that includes retries (the `timeout` option is per
257
+ attempt only):
258
+
259
+ ```typescript
260
+ const result = await client.web.search('latest news', {
261
+ signal: AbortSignal.timeout(5_000),
262
+ });
263
+ ```
264
+
265
+ Combine multiple signals (e.g. a request signal plus a wall-clock cap):
266
+
267
+ ```typescript
268
+ const result = await client.web.search('latest news', {
269
+ signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]),
270
+ });
271
+ ```
272
+
273
+ ### Custom fetch (proxy, TLS, ...)
274
+
275
+ For advanced HTTP settings — proxy, custom TLS, mTLS, etc. — pass a custom
276
+ `fetchFn` that wraps the global `fetch`. Spread `init` in your wrapper
277
+ so headers, body, and the internal `AbortSignal` are preserved.
278
+
279
+ ```typescript
280
+ import { ProxyAgent } from 'undici';
281
+ import { WebIQClient } from '@microsoft/webiq';
282
+
283
+ const proxyAgent = new ProxyAgent('http://my-proxy:8080');
284
+
285
+ const client = new WebIQClient({
286
+ apiKey: 'your-api-key',
287
+ // `dispatcher` is a Node/undici-specific init field; the `FetchFunction`
288
+ // type accepts extra fields so no type assertion is needed.
289
+ fetchFn: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
290
+ });
291
+ ```
292
+
293
+ ### Telemetry
294
+
295
+ ```typescript
296
+ import { WebIQClient, TelemetryEvent } from '@microsoft/webiq';
297
+
298
+ const client = new WebIQClient({
299
+ apiKey: 'your-api-key',
300
+ telemetryHook: (event: TelemetryEvent) => {
301
+ console.log(`${event.method} ${event.path} → ${event.statusCode} (${event.elapsedMs}ms)`);
302
+ },
303
+ });
304
+ ```
305
+
306
+ ## Error Handling
307
+
308
+ The SDK throws specific error types for different error conditions. All errors extend `WebIQError`.
309
+
310
+ ### Error Hierarchy
311
+
312
+ | Error Class | HTTP Status | When |
313
+ | ----------------------- | ---------------------------- | ------------------------------------------------- |
314
+ | `AuthenticationError` | 401 | Invalid or missing API key |
315
+ | `PermissionDeniedError` | 403 | Authenticated but not authorized for the resource |
316
+ | `RateLimitError` | 429, 430 | Rate limit / concurrent-request limit exceeded |
317
+ | `APIStatusError` | 400, 404, 500, 503, 504, ... | All other HTTP errors |
318
+ | `APIConnectionError` | — | Network issues, DNS failures, timeouts |
319
+ | `WebIQError` | — | Base class for all SDK errors |
320
+
321
+ `PermissionDeniedError` and `RateLimitError` both extend `APIStatusError`, so a broader `instanceof APIStatusError` check still matches them.
322
+
323
+ ### Rate limits are never auto-retried
324
+
325
+ The SDK does **not** automatically retry `429` (rate limit) or `430` (concurrent-request limit) responses. As soon as the API returns one, the transport throws `RateLimitError` so your application can decide what to do — back off, queue the request, surface it to the user, etc. Generic retry settings (`RetryPolicy.retryOnStatus`, `maxRetries`) do **not** apply to rate limits.
326
+
327
+ The server reports the back-off hint in the response **body** as the `retryAfter` field — typically a duration with an `s` suffix (e.g. `"30s"`, `"60s"`). The SDK surfaces that value unchanged on `error.retryAfter`.
328
+
329
+ ```typescript
330
+ import { RateLimitError } from '@microsoft/webiq';
331
+
332
+ try {
333
+ const results = await client.web.search('test');
334
+ } catch (error) {
335
+ if (error instanceof RateLimitError) {
336
+ // error.retryAfter is the server-provided value from the response body
337
+ // (e.g. "60s"). The SDK does not parse or normalize it for you.
338
+ console.warn(`Rate limited. Retry after: ${error.retryAfter}`);
339
+ // Your retry strategy lives here — the SDK will not retry for you.
340
+ }
341
+ }
342
+ ```
343
+
344
+ ### Basic Error Handling
345
+
346
+ ```typescript
347
+ import {
348
+ WebIQError,
349
+ APIConnectionError,
350
+ APIStatusError,
351
+ AuthenticationError,
352
+ PermissionDeniedError,
353
+ RateLimitError,
354
+ } from '@microsoft/webiq';
355
+
356
+ try {
357
+ const results = await client.web.search('test');
358
+ } catch (error) {
359
+ if (error instanceof PermissionDeniedError) {
360
+ // 403 — authenticated, but not allowed to call this resource
361
+ console.error(`Forbidden (HTTP ${error.statusCode}):`, error.message);
362
+ } else if (error instanceof AuthenticationError) {
363
+ // 401 — invalid or missing API key
364
+ console.error(`Auth failed (HTTP ${error.statusCode}):`, error.message);
365
+ } else if (error instanceof RateLimitError) {
366
+ // 429 or 430 — rate limit / concurrent-request limit (never auto-retried)
367
+ console.error(`Rate limited. Retry after: ${error.retryAfter}`);
368
+ } else if (error instanceof APIStatusError) {
369
+ // Other HTTP errors (400, 404, 500, 503, 504, etc.)
370
+ console.error(`API error (HTTP ${error.statusCode}):`, error.message);
371
+ } else if (error instanceof APIConnectionError) {
372
+ // Network issues
373
+ console.error('Connection failed:', error.message);
374
+ }
375
+ }
376
+ ```
377
+
378
+ ### Inspecting Error Details
379
+
380
+ All `APIStatusError` instances (including `PermissionDeniedError` and `RateLimitError`) expose the full error response body. The pattern below applies when the **server** rejects a request (e.g. a 4xx the SDK couldn't pre-validate, or a transient 5xx). `browse.fetch` is a convenient way to trigger one — a missing or filtered URL surfaces as a structured error:
381
+
382
+ ```typescript
383
+ import { WebIQClient, APIStatusError } from '@microsoft/webiq';
384
+
385
+ async function main() {
386
+ const client = new WebIQClient({ apiKey: 'your-api-key' });
387
+
388
+ try {
389
+ await client.browse.fetch('https://www.not-microsoft.com');
390
+ } catch (error) {
391
+ if (error instanceof APIStatusError) {
392
+ console.error(`Status: ${error.statusCode}`); // e.g. 404
393
+ console.error(`Message: ${error.message}`); // e.g. "No result is found"
394
+
395
+ // Full error body from the API response
396
+ if (error.body && typeof error.body === 'object') {
397
+ const body = error.body as Record<string, unknown>;
398
+ console.error(`Error code: ${body.errorCode}`); // e.g. "BrowseApiDocNotFound"
399
+ console.error(`Category: ${body.errorCategory}`); // e.g. "UserError"
400
+ console.error(`Details: ${body.technicalDetails}`); // e.g. "NotFound"
401
+ console.error(`Trace ID: ${body.traceId}`); // for debugging with support
402
+ console.error(`Retry after: ${body.retryAfter}`); // for retryable errors
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ main().catch(console.error);
409
+ ```
410
+
411
+ ## Concurrent Requests
412
+
413
+ ```typescript
414
+ const [web, news, videos] = await Promise.all([
415
+ client.web.search('TypeScript'),
416
+ client.news.search('programming'),
417
+ client.videos.search('tutorial'),
418
+ ]);
419
+ ```
420
+
421
+ ## License
422
+
423
+ MIT