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