@microsoft/webiq 0.1.8 → 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 +180 -62
- package/dist/index.d.mts +191 -10
- package/dist/index.d.ts +191 -10
- package/dist/index.js +395 -10
- package/dist/index.mjs +379 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript SDK for Web IQ APIs.
|
|
4
4
|
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](#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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
For an HTTP framework that supplies a Fetch API `Request`, propagate its cancellation signal:
|
|
217
243
|
|
|
218
244
|
```typescript
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
235
|
-
attempt
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
308
|
+
import { WebIQClient } from '@microsoft/webiq';
|
|
275
309
|
|
|
276
310
|
const client = new WebIQClient({
|
|
277
|
-
apiKey:
|
|
278
|
-
telemetryHook: (event
|
|
279
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
291
|
-
| ----------------------- |
|
|
292
|
-
| `
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
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`
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
473
|
+
import { WebIQClient, AuthenticationError } from '@microsoft/webiq';
|
|
362
474
|
|
|
363
475
|
async function main() {
|
|
364
|
-
const client = new WebIQClient({ apiKey: '
|
|
476
|
+
const client = new WebIQClient({ apiKey: 'invalid-api-key' });
|
|
365
477
|
|
|
366
478
|
try {
|
|
367
|
-
await client.
|
|
479
|
+
await client.web.search('Microsoft Web IQ');
|
|
368
480
|
} catch (error) {
|
|
369
|
-
if (error instanceof
|
|
370
|
-
console.error(
|
|
371
|
-
console.error(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|