@microsoft/webiq 0.1.7 → 1.0.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
@@ -2,11 +2,16 @@
2
2
 
3
3
  Official TypeScript SDK for Web IQ APIs.
4
4
 
5
- [![Node.js 22+](https://img.shields.io/badge/node-22+-green.svg)](https://nodejs.org/)
5
+ [![Node.js ^22.13.0 or >=24](https://img.shields.io/badge/node-%5E22.13.0%20%7C%7C%20%3E%3D24-green.svg)](https://nodejs.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license)
7
7
 
8
+ For product guides and API documentation, see
9
+ [Microsoft Web IQ documentation](https://learn.microsoft.com/en-us/web-iq/).
10
+
8
11
  ## Installation
9
12
 
13
+ Requires Node.js 22.x starting at 22.13.0, or Node.js 24 or later, and npm 10+.
14
+
10
15
  ```bash
11
16
  npm install @microsoft/webiq
12
17
  ```
@@ -52,9 +57,12 @@ await client.close();
52
57
 
53
58
  ## Authentication
54
59
 
55
- ### API Key
60
+ For Web IQ authentication, see
61
+ [Authenticate with Microsoft Web IQ](https://learn.microsoft.com/en-us/web-iq/get-started/authentication).
62
+
63
+ The SDK supports API key and Microsoft Entra ID authentication.
56
64
 
57
- Get an API key at [webiq.microsoft.ai](https://webiq.microsoft.ai).
65
+ ### API Key
58
66
 
59
67
  ```typescript
60
68
  import { WebIQClient } from '@microsoft/webiq';
@@ -64,9 +72,26 @@ const client = new WebIQClient({
64
72
  });
65
73
  ```
66
74
 
67
- ### EntraID (Azure AD)
75
+ ### Microsoft Entra ID
76
+
77
+ Install the optional Azure credential package:
78
+
79
+ ```bash
80
+ npm install "@azure/identity@>=4.13.1"
81
+ ```
82
+
83
+ Pass a `TokenCredential`, such as `DefaultAzureCredential`:
84
+
85
+ ```typescript
86
+ import { DefaultAzureCredential } from '@azure/identity';
87
+ import { WebIQClient } from '@microsoft/webiq';
88
+
89
+ const client = new WebIQClient({
90
+ credential: new DefaultAzureCredential(),
91
+ });
92
+ ```
68
93
 
69
- Pass any `@azure/identity` `TokenCredential`:
94
+ ### Calling more than one AppID
70
95
 
71
96
  ```typescript
72
97
  import { DefaultAzureCredential } from '@azure/identity';
@@ -74,7 +99,11 @@ import { WebIQClient } from '@microsoft/webiq';
74
99
 
75
100
  const client = new WebIQClient({
76
101
  credential: new DefaultAzureCredential(),
102
+ appId: 'your-app-id',
77
103
  });
104
+
105
+ await client.web.search('TypeScript programming');
106
+ await client.close();
78
107
  ```
79
108
 
80
109
  ## API Reference
@@ -92,9 +121,6 @@ const results = await client.web.search(query, {
92
121
  contentFormat: ContentFormat.HTML, // PASSAGE, TEXT, HTML, MARKDOWN
93
122
  maxLength: 10000, // 1-500000
94
123
  safeSearch: SafeSearch.STRICT, // OFF, STRICT
95
- customSearchConfigId: 'my-config', // Optional custom search config
96
- includeDomains: ['example.com'], // Optional allowlist (max 250)
97
- excludeDomains: ['spam.example'], // Optional blocklist (max 250)
98
124
  });
99
125
  // results.webResults → [{title, url, content, lastUpdatedAt, crawledAt, language, isAdult, contentTier}]
100
126
  ```
@@ -213,26 +239,21 @@ Every search and browse method accepts an `AbortSignal` via `options.signal`.
213
239
  When the signal aborts, the in-flight request is cancelled, retries stop, and
214
240
  the signal's `reason` is re-thrown unchanged (no `APIConnectionError` wrapping).
215
241
 
216
- Propagate a client disconnect from an HTTP handler:
242
+ For an HTTP framework that supplies a Fetch API `Request`, propagate its cancellation signal:
217
243
 
218
244
  ```typescript
219
- import express from 'express';
220
-
221
- app.get('/search', async (req, res) => {
222
- try {
223
- const result = await client.web.search(req.query.q as string, {
224
- signal: req.signal, // express/fastify expose the request's AbortSignal
225
- });
226
- res.json(result);
227
- } catch (err) {
228
- if (err instanceof DOMException && err.name === 'AbortError') return; // client gone
229
- throw err;
245
+ async function handleSearch(request: Request): Promise<Response> {
246
+ const query = new URL(request.url).searchParams.get('q');
247
+ if (!query) {
248
+ return new Response('Missing search query', { status: 400 });
230
249
  }
231
- });
250
+ const result = await client.web.search(query, { signal: request.signal });
251
+ return Response.json(result);
252
+ }
232
253
  ```
233
254
 
234
- Apply a wall-clock budget that includes retries (the `timeout` option is per
235
- attempt only):
255
+ Use a timeout signal shared across retry attempts (the `timeout` option resets
256
+ for each attempt):
236
257
 
237
258
  ```typescript
238
259
  const result = await client.web.search('latest news', {
@@ -240,11 +261,13 @@ const result = await client.web.search('latest news', {
240
261
  });
241
262
  ```
242
263
 
243
- Combine multiple signals (e.g. a request signal plus a wall-clock cap):
264
+ An active retry delay or credential lookup may finish before cancellation is observed.
265
+
266
+ Combine multiple signals (e.g. a request signal plus a timeout):
244
267
 
245
268
  ```typescript
246
269
  const result = await client.web.search('latest news', {
247
- signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]),
270
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(5_000)]),
248
271
  });
249
272
  ```
250
273
 
@@ -254,6 +277,12 @@ For advanced HTTP settings — proxy, custom TLS, mTLS, etc. — pass a custom
254
277
  `fetchFn` that wraps the global `fetch`. Spread `init` in your wrapper
255
278
  so headers, body, and the internal `AbortSignal` are preserved.
256
279
 
280
+ Install the optional proxy package for this example:
281
+
282
+ ```bash
283
+ npm install undici
284
+ ```
285
+
257
286
  ```typescript
258
287
  import { ProxyAgent } from 'undici';
259
288
  import { WebIQClient } from '@microsoft/webiq';
@@ -262,47 +291,124 @@ const proxyAgent = new ProxyAgent('http://my-proxy:8080');
262
291
 
263
292
  const client = new WebIQClient({
264
293
  apiKey: 'your-api-key',
265
- // `dispatcher` is a Node/undici-specific init field; the `FetchFunction`
266
- // type accepts extra fields so no type assertion is needed.
267
- fetchFn: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
294
+ fetchFn: (url, init) => {
295
+ // Keep the Node-specific option in a variable so this also type-checks
296
+ // when the project includes DOM fetch types.
297
+ const proxyInit = { ...init, dispatcher: proxyAgent };
298
+ return fetch(url, proxyInit);
299
+ },
268
300
  });
269
301
  ```
270
302
 
271
303
  ### Telemetry
272
304
 
305
+ Set `telemetryHook` to observe each HTTP request attempt:
306
+
273
307
  ```typescript
274
- import { WebIQClient, TelemetryEvent } from '@microsoft/webiq';
308
+ import { WebIQClient } from '@microsoft/webiq';
275
309
 
276
310
  const client = new WebIQClient({
277
- apiKey: 'your-api-key',
278
- telemetryHook: (event: TelemetryEvent) => {
279
- console.log(`${event.method} ${event.path} → ${event.statusCode} (${event.elapsedMs}ms)`);
311
+ apiKey: process.env.WEBIQ_API_KEY,
312
+ telemetryHook: (event) => {
313
+ console.log(
314
+ `${event.method} ${event.path}: ` +
315
+ `status=${event.statusCode}, attempt=${event.attempt}, elapsed=${event.elapsedMs}ms`,
316
+ );
280
317
  },
281
318
  });
319
+
320
+ await client.web.search('TypeScript programming');
321
+ await client.close();
322
+ ```
323
+
324
+ Each `TelemetryEvent` carries `method`, `path`, `transport`, `attempt` (1-based; increments on every retry), `statusCode`, `requestId`, `traceId`, `retryAfter`, `elapsedMs` (per-attempt duration), `responseSizeBytes` (response body size in bytes, set whenever a response is received), `error`, and `errorBody`.
325
+
326
+ ## Click and citation feedback
327
+
328
+ For instrumentation setup and behavior, see
329
+ [Instrument Microsoft Web IQ usage](https://learn.microsoft.com/en-us/web-iq/guides/instrumentation).
330
+
331
+ These snippets use an open `client`, the original search `response`, and a
332
+ `result` from that response.
333
+
334
+ ### Click
335
+
336
+ Call when the user clicks `result`. Use the returned navigation URL in your
337
+ application's router or redirect response.
338
+
339
+ ```typescript
340
+ const click = await client.feedback.pushEvents({
341
+ response,
342
+ type: 'click',
343
+ events: [{ result }],
344
+ navigation: 'manual',
345
+ failureMode: 'throw',
346
+ });
347
+
348
+ const outcome = click.outcomes[0];
349
+ if (outcome?.type === 'click') {
350
+ console.log('Navigate to:', outcome.navigationUrl);
351
+ }
352
+ ```
353
+
354
+ ### Citation
355
+
356
+ Call when the generated answer actually cites `result`.
357
+
358
+ ```typescript
359
+ if (response.instrumentationCitationBase) {
360
+ await client.feedback.pushEvents({
361
+ response,
362
+ type: 'citation',
363
+ events: [{ results: [result] }],
364
+ failureMode: 'throw',
365
+ });
366
+ }
282
367
  ```
283
368
 
284
369
  ## Error Handling
285
370
 
286
- The SDK throws specific error types for different error conditions. All errors extend `WebIQError`.
371
+ For detailed error definition, see [Microsoft Web IQ error handling](https://learn.microsoft.com/en-us/web-iq/reference/error-handling)
372
+
373
+ The SDK's API and connection error classes extend `WebIQError`. Invalid client configuration throws a standard `Error`, and caller cancellation propagates the signal's reason unchanged.
287
374
 
288
375
  ### Error Hierarchy
289
376
 
290
- | Error Class | HTTP Status | When |
291
- | ----------------------- | ---------------------------- | ------------------------------------------------- |
292
- | `AuthenticationError` | 401 | Invalid or missing API key |
293
- | `PermissionDeniedError` | 403 | Authenticated but not authorized for the resource |
294
- | `RateLimitError` | 429, 430 | Rate limit / concurrent-request limit exceeded |
295
- | `APIStatusError` | 400, 404, 500, 503, 504, ... | All other HTTP errors |
296
- | `APIConnectionError` | — | Network issues, DNS failures, timeouts |
297
- | `WebIQError` | — | Base class for all SDK errors |
377
+ | Error Class | HTTP Status | When |
378
+ | ----------------------- | ----------- | --------------------------------------------------- |
379
+ | `BadRequestError` | 400 | Invalid request |
380
+ | `AuthenticationError` | 401 | Invalid or missing API key |
381
+ | `PermissionDeniedError` | 403 | Authenticated but not authorized for the resource |
382
+ | `NotFoundError` | 404 | Requested resource or Browse document was not found |
383
+ | `RateLimitError` | 429, 430 | Rate limit / concurrent-request limit exceeded |
384
+ | `APIStatusError` | 500, ... | Other HTTP errors |
385
+ | `APIConnectionError` | — | Network issues, DNS failures, timeouts |
386
+ | `WebIQError` | — | Base class for SDK HTTP and connection errors |
298
387
 
299
- `PermissionDeniedError` and `RateLimitError` both extend `APIStatusError`, so a broader `instanceof APIStatusError` check still matches them.
388
+ `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, and `RateLimitError` all extend `APIStatusError`, so a broader `instanceof APIStatusError` check still matches them.
389
+
390
+ ### Retries and Browse responses
391
+
392
+ The default retry statuses are `408`, `500`, `502`, `520`, `523`, and `524`.
393
+ The latter three cover granular Browse system errors. A valid body `retryAfter`
394
+ is honored in full; `maxDelaySeconds` caps only generated exponential backoff.
395
+ Timeouts apply per attempt, not to server-directed waits, which can be long.
396
+ Use `maxRetries: 0` to manage retry deadlines in your application, or provide an
397
+ `AbortSignal` to cancel the call, including retry waits.
398
+
399
+ HTTP 400 and 404 are not retried by default, but explicit `retryOnStatus`
400
+ selections are honored. `BadRequestError` and `NotFoundError` are thrown after
401
+ the configured retry budget is exhausted.
402
+
403
+ Browse `202 Accepted` returns a normal response without automatic polling.
404
+ Use its `url` and UTC `retryAfter` timestamp to poll later. Partial HTTP 200
405
+ responses can include `truncationReason`, such as `"encumbrance"`.
300
406
 
301
407
  ### Rate limits are never auto-retried
302
408
 
303
409
  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.
304
410
 
305
- 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`.
411
+ The server reports the back-off hint in the response **body** as the optional `retryAfter` field: a duration with an `s` suffix (e.g. `"30s"`, `"60s"`) or, for granular Browse errors, an ISO8601 UTC timestamp. The SDK surfaces that value unchanged on `error.retryAfter`.
306
412
 
307
413
  ```typescript
308
414
  import { RateLimitError } from '@microsoft/webiq';
@@ -327,6 +433,8 @@ import {
327
433
  APIConnectionError,
328
434
  APIStatusError,
329
435
  AuthenticationError,
436
+ BadRequestError,
437
+ NotFoundError,
330
438
  PermissionDeniedError,
331
439
  RateLimitError,
332
440
  } from '@microsoft/webiq';
@@ -334,17 +442,21 @@ import {
334
442
  try {
335
443
  const results = await client.web.search('test');
336
444
  } catch (error) {
337
- if (error instanceof PermissionDeniedError) {
445
+ if (error instanceof BadRequestError) {
446
+ console.error(`Bad request (HTTP ${error.statusCode}):`, error.message);
447
+ } else if (error instanceof PermissionDeniedError) {
338
448
  // 403 — authenticated, but not allowed to call this resource
339
449
  console.error(`Forbidden (HTTP ${error.statusCode}):`, error.message);
340
450
  } else if (error instanceof AuthenticationError) {
341
451
  // 401 — invalid or missing API key
342
452
  console.error(`Auth failed (HTTP ${error.statusCode}):`, error.message);
453
+ } else if (error instanceof NotFoundError) {
454
+ console.error(`Not found (HTTP ${error.statusCode}):`, error.message);
343
455
  } else if (error instanceof RateLimitError) {
344
456
  // 429 or 430 — rate limit / concurrent-request limit (never auto-retried)
345
457
  console.error(`Rate limited. Retry after: ${error.retryAfter}`);
346
458
  } else if (error instanceof APIStatusError) {
347
- // Other HTTP errors (400, 404, 500, 503, 504, etc.)
459
+ // Other HTTP errors (500, etc.)
348
460
  console.error(`API error (HTTP ${error.statusCode}):`, error.message);
349
461
  } else if (error instanceof APIConnectionError) {
350
462
  // Network issues
@@ -355,30 +467,25 @@ try {
355
467
 
356
468
  ### Inspecting Error Details
357
469
 
358
- 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:
470
+ All `APIStatusError` instances expose `errorCategory`, `errorCode`, `userMessage`, `technicalDetails`, `retryAfter`, `requestId`, and `traceId` directly, without validating an error-response model. Missing optional fields are `undefined`; the original response remains available in `body`. The following example uses an invalid API key to demonstrate a 401 authentication error.
359
471
 
360
472
  ```typescript
361
- import { WebIQClient, APIStatusError } from '@microsoft/webiq';
473
+ import { WebIQClient, AuthenticationError } from '@microsoft/webiq';
362
474
 
363
475
  async function main() {
364
- const client = new WebIQClient({ apiKey: 'your-api-key' });
476
+ const client = new WebIQClient({ apiKey: 'invalid-api-key' });
365
477
 
366
478
  try {
367
- await client.browse.fetch('https://www.not-microsoft.com');
479
+ await client.web.search('Microsoft Web IQ');
368
480
  } catch (error) {
369
- if (error instanceof APIStatusError) {
370
- console.error(`Status: ${error.statusCode}`); // e.g. 404
371
- console.error(`Message: ${error.message}`); // e.g. "No result is found"
372
-
373
- // Full error body from the API response
374
- if (error.body && typeof error.body === 'object') {
375
- const body = error.body as Record<string, unknown>;
376
- console.error(`Error code: ${body.errorCode}`); // e.g. "BrowseApiDocNotFound"
377
- console.error(`Category: ${body.errorCategory}`); // e.g. "UserError"
378
- console.error(`Details: ${body.technicalDetails}`); // e.g. "NotFound"
379
- console.error(`Trace ID: ${body.traceId}`); // for debugging with support
380
- console.error(`Retry after: ${body.retryAfter}`); // for retryable errors
381
- }
481
+ if (error instanceof AuthenticationError) {
482
+ console.error(error.statusCode); // 401
483
+ console.error(error.errorCategory);
484
+ console.error(error.errorCode);
485
+ console.error(error.userMessage);
486
+ console.error(error.traceId);
487
+ } else {
488
+ throw error;
382
489
  }
383
490
  }
384
491
  }
@@ -399,3 +506,14 @@ const [web, news, videos] = await Promise.all([
399
506
  ## License
400
507
 
401
508
  MIT
509
+
510
+ ## Release History
511
+
512
+ ### 1.0.0
513
+
514
+ #### Features Added
515
+
516
+ - Web, news, video, and image search, plus page browsing.
517
+ - API key and Microsoft Entra ID authentication.
518
+ - Click and citation feedback.
519
+ - Error handling and telemetry support