@datagrout/conduit 0.7.0 → 0.8.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 +63 -3
- package/dist/index.d.mts +967 -116
- package/dist/index.d.ts +967 -116
- package/dist/index.js +1308 -69
- package/dist/index.mjs +1287 -71
- package/package.json +10 -9
package/dist/index.d.mts
CHANGED
|
@@ -217,6 +217,837 @@ declare class ConduitIdentity {
|
|
|
217
217
|
*/
|
|
218
218
|
declare function fetchWithIdentity(url: string, init: RequestInit, identity: ConduitIdentity): Promise<Response>;
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Typed error classes for the DataGrout Conduit SDK.
|
|
222
|
+
*
|
|
223
|
+
* All errors extend `ConduitError` so callers can catch the whole family with
|
|
224
|
+
* a single `instanceof ConduitError` check, or target specific subclasses.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Base class for all errors thrown by the Conduit SDK.
|
|
229
|
+
*
|
|
230
|
+
* Fixes the prototype chain so `instanceof` works correctly when compiling
|
|
231
|
+
* to CommonJS / ES5 targets.
|
|
232
|
+
*/
|
|
233
|
+
declare class ConduitError extends Error {
|
|
234
|
+
constructor(message: string);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Thrown when a `Client` method is called before `connect()` has been invoked,
|
|
238
|
+
* or after `disconnect()` has been called.
|
|
239
|
+
*/
|
|
240
|
+
declare class NotInitializedError extends ConduitError {
|
|
241
|
+
constructor();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Thrown when the DataGrout gateway returns HTTP 429 (Too Many Requests).
|
|
245
|
+
*
|
|
246
|
+
* Authenticated DataGrout users are never rate-limited. Unauthenticated
|
|
247
|
+
* callers hitting the hourly cap will receive this error.
|
|
248
|
+
*
|
|
249
|
+
* @property status - Parsed rate-limit header state.
|
|
250
|
+
* @property retryAfter - Seconds to wait before retrying (from `Retry-After` header), if present.
|
|
251
|
+
*/
|
|
252
|
+
declare class RateLimitError extends ConduitError {
|
|
253
|
+
readonly status: RateLimitStatus;
|
|
254
|
+
readonly retryAfter?: number;
|
|
255
|
+
constructor(status: RateLimitStatus, retryAfter?: number);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Thrown when the server returns HTTP 401 Unauthorized or HTTP 403 Forbidden.
|
|
259
|
+
*/
|
|
260
|
+
declare class AuthError extends ConduitError {
|
|
261
|
+
constructor(message?: string);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Thrown on network-level failures such as fetch errors, connection refused,
|
|
265
|
+
* or request timeouts.
|
|
266
|
+
*/
|
|
267
|
+
declare class NetworkError extends ConduitError {
|
|
268
|
+
constructor(message: string);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Thrown when the server returns an unexpected non-success HTTP status or
|
|
272
|
+
* a JSON-RPC error payload.
|
|
273
|
+
*
|
|
274
|
+
* @property code - HTTP status code or JSON-RPC error code.
|
|
275
|
+
* @property serverMessage - Raw error message from the server.
|
|
276
|
+
*/
|
|
277
|
+
declare class ServerError extends ConduitError {
|
|
278
|
+
readonly code: number;
|
|
279
|
+
readonly serverMessage: string;
|
|
280
|
+
constructor(code: number, serverMessage: string);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Thrown when required parameters are missing, mutually-exclusive option
|
|
284
|
+
* combinations are invalid, or a method receives an unusable configuration.
|
|
285
|
+
*/
|
|
286
|
+
declare class InvalidConfigError extends ConduitError {
|
|
287
|
+
constructor(message: string);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* OAuth 2.1 **authorization code + PKCE** — browser-consent sign-in.
|
|
292
|
+
*
|
|
293
|
+
* The `client_credentials` grant in {@link ./oauth} authenticates a *machine*:
|
|
294
|
+
* it needs a client secret issued out of band. This module authenticates a
|
|
295
|
+
* *person*: the app opens a browser, the user consents at the gateway, and the
|
|
296
|
+
* app receives a grant bound to that user's account. It is what a desktop or
|
|
297
|
+
* CLI application needs, and the only way to use
|
|
298
|
+
* `https://gateway.datagrout.ai/connect`, where the server binding is chosen at
|
|
299
|
+
* consent time and lives in the token rather than the URL.
|
|
300
|
+
*
|
|
301
|
+
* # Flow
|
|
302
|
+
*
|
|
303
|
+
* 1. {@link AuthCodeFlow.discover} — protected-resource metadata, then the
|
|
304
|
+
* authorization server's metadata.
|
|
305
|
+
* 2. {@link AuthCodeFlow.register} — RFC 7591 dynamic client registration, as a
|
|
306
|
+
* **public client** (no secret; PKCE takes its place).
|
|
307
|
+
* 3. {@link AuthCodeFlow.authorizeUrl} — build the consent URL and hold the
|
|
308
|
+
* PKCE verifier and CSRF state in a {@link PendingAuthorization}.
|
|
309
|
+
* 4. The caller opens that URL and captures the redirect. `authcode/loopback`
|
|
310
|
+
* can do the capturing.
|
|
311
|
+
* 5. {@link AuthCodeFlow.exchange} — trade the code for a {@link Grant}.
|
|
312
|
+
*
|
|
313
|
+
* ```ts
|
|
314
|
+
* import { AuthCodeFlow } from "@datagrout/conduit/authcode";
|
|
315
|
+
*
|
|
316
|
+
* const flow = await AuthCodeFlow.discover("https://gateway.datagrout.ai/connect");
|
|
317
|
+
* const client = await flow.register("My App", "http://127.0.0.1:8765/callback");
|
|
318
|
+
*
|
|
319
|
+
* const { url, pending } = flow.authorizeUrl();
|
|
320
|
+
* console.log(`Open: ${url}`);
|
|
321
|
+
*
|
|
322
|
+
* const grant = await flow.exchange(pending, code, state);
|
|
323
|
+
* ```
|
|
324
|
+
*
|
|
325
|
+
* # Persisting the grant
|
|
326
|
+
*
|
|
327
|
+
* This module owns the {@link Grant} shape and its refresh logic; it
|
|
328
|
+
* deliberately does **not** choose where a grant is stored. That is the
|
|
329
|
+
* application's decision — an OS keychain, a config file, a vault — and baking
|
|
330
|
+
* a filesystem opinion into an SDK makes it wrong for half its callers.
|
|
331
|
+
*
|
|
332
|
+
* {@link Grant.expires_at} is Unix **seconds** rather than a monotonic clock
|
|
333
|
+
* value, precisely so a grant survives serialization: it is written by one
|
|
334
|
+
* process and read by another, possibly in a different language.
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Scopes requested when the caller does not specify.
|
|
339
|
+
*
|
|
340
|
+
* Matches the authorization server's own registration default rather than
|
|
341
|
+
* inventing a finer-grained vocabulary: DataGrout splits the scope string on
|
|
342
|
+
* whitespace and stores what it is given, so a made-up scope is accepted
|
|
343
|
+
* silently and then means nothing.
|
|
344
|
+
*/
|
|
345
|
+
declare const DEFAULT_SCOPE = "mcp tools";
|
|
346
|
+
/**
|
|
347
|
+
* The distinguishable failures of the authorization-code flow.
|
|
348
|
+
*
|
|
349
|
+
* The taxonomy is part of the cross-language contract: every conduit SDK
|
|
350
|
+
* distinguishes these same cases, so callers can branch identically.
|
|
351
|
+
*/
|
|
352
|
+
type AuthCodeErrorKind =
|
|
353
|
+
/** Metadata discovery failed or returned something unusable. */
|
|
354
|
+
"discovery"
|
|
355
|
+
/** The authorization server does not advertise dynamic client registration. */
|
|
356
|
+
| "no_registration_endpoint"
|
|
357
|
+
/** Dynamic client registration was rejected. */
|
|
358
|
+
| "registration_rejected"
|
|
359
|
+
/** `authorizeUrl` was called before a client id was known. */
|
|
360
|
+
| "no_client_id"
|
|
361
|
+
/** The server does not support PKCE with S256. */
|
|
362
|
+
| "pkce_unsupported"
|
|
363
|
+
/** The `state` returned by the redirect did not match the one sent. */
|
|
364
|
+
| "state_mismatch"
|
|
365
|
+
/** The token endpoint rejected the exchange or refresh. */
|
|
366
|
+
| "token_exchange"
|
|
367
|
+
/** The grant has no refresh token, so it cannot be renewed. */
|
|
368
|
+
| "not_refreshable"
|
|
369
|
+
/** The authorization server returned an error at the redirect. */
|
|
370
|
+
| "denied"
|
|
371
|
+
/** Transport failure talking to the authorization server. */
|
|
372
|
+
| "http";
|
|
373
|
+
/** An error from the authorization-code flow, tagged with its {@link AuthCodeErrorKind}. */
|
|
374
|
+
declare class AuthCodeError extends ConduitError {
|
|
375
|
+
readonly kind: AuthCodeErrorKind;
|
|
376
|
+
/** HTTP status, for `registration_rejected` and `token_exchange`. */
|
|
377
|
+
readonly status?: number;
|
|
378
|
+
/** Response body, for `registration_rejected` and `token_exchange`. */
|
|
379
|
+
readonly body?: string;
|
|
380
|
+
constructor(kind: AuthCodeErrorKind, message: string, extra?: {
|
|
381
|
+
status?: number;
|
|
382
|
+
body?: string;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/** RFC 8414 authorization server metadata (the fields this flow uses). */
|
|
386
|
+
interface AuthServerMetadata {
|
|
387
|
+
issuer?: string;
|
|
388
|
+
authorization_endpoint: string;
|
|
389
|
+
token_endpoint: string;
|
|
390
|
+
/** RFC 7591 dynamic client registration endpoint, when offered. */
|
|
391
|
+
registration_endpoint?: string;
|
|
392
|
+
/** PKCE methods, e.g. `["S256"]`. */
|
|
393
|
+
code_challenge_methods_supported?: string[];
|
|
394
|
+
grant_types_supported?: string[];
|
|
395
|
+
scopes_supported?: string[];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Whether S256 is usable.
|
|
399
|
+
*
|
|
400
|
+
* An empty or absent list means the server did not advertise. RFC 8414 makes
|
|
401
|
+
* the field optional and DataGrout omits it on some paths, so absence is
|
|
402
|
+
* treated as "assume S256" rather than as a refusal — a server that truly
|
|
403
|
+
* cannot do S256 will reject the authorize request anyway.
|
|
404
|
+
*/
|
|
405
|
+
declare function supportsS256(metadata: AuthServerMetadata): boolean;
|
|
406
|
+
/**
|
|
407
|
+
* A dynamically-registered client: the id **and** the redirect URI it is bound
|
|
408
|
+
* to.
|
|
409
|
+
*
|
|
410
|
+
* These travel together because an authorization server matches the redirect
|
|
411
|
+
* URI **exactly** against the value registered — there is no loopback-port
|
|
412
|
+
* exemption to rely on. Persisting the id alone means a later re-authorization
|
|
413
|
+
* on a freshly-chosen port is rejected as `invalid_redirect_uri`, and the
|
|
414
|
+
* failure only shows up once the first grant can no longer be refreshed.
|
|
415
|
+
*/
|
|
416
|
+
interface RegisteredClient {
|
|
417
|
+
client_id: string;
|
|
418
|
+
redirect_uri: string;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* A user's authorization, ready to persist.
|
|
422
|
+
*
|
|
423
|
+
* The serialized shape is part of the cross-language contract: a grant written
|
|
424
|
+
* by one conduit SDK must be readable by another. Field names are therefore
|
|
425
|
+
* snake_case and fixed, and `expires_at` is Unix seconds.
|
|
426
|
+
*/
|
|
427
|
+
interface Grant {
|
|
428
|
+
access_token: string;
|
|
429
|
+
refresh_token?: string;
|
|
430
|
+
/** Absolute expiry, Unix seconds. Absent means the server did not say. */
|
|
431
|
+
expires_at?: number;
|
|
432
|
+
/** The client id this grant belongs to — needed to refresh it. */
|
|
433
|
+
client_id: string;
|
|
434
|
+
/** Token endpoint that issued it — needed to refresh it. */
|
|
435
|
+
token_endpoint: string;
|
|
436
|
+
scope?: string;
|
|
437
|
+
/** The resource this grant is bound to (RFC 8707). */
|
|
438
|
+
resource?: string;
|
|
439
|
+
}
|
|
440
|
+
/** Anything with `fetch`'s shape, so tests can substitute a fake. */
|
|
441
|
+
type FetchLike = typeof globalThis.fetch;
|
|
442
|
+
/**
|
|
443
|
+
* True when the access token is expired, or within the refresh skew of it.
|
|
444
|
+
*
|
|
445
|
+
* A grant with no stated expiry is treated as live: the server chose not to
|
|
446
|
+
* say, and guessing an expiry would throw away working tokens.
|
|
447
|
+
*/
|
|
448
|
+
declare function isGrantExpired(grant: Grant): boolean;
|
|
449
|
+
/** Whether this grant can renew itself without user interaction. */
|
|
450
|
+
declare function isGrantRefreshable(grant: Grant): boolean;
|
|
451
|
+
/**
|
|
452
|
+
* Exchange the refresh token for a fresh grant.
|
|
453
|
+
*
|
|
454
|
+
* Returns a new grant; the old one should be discarded. DataGrout rotates
|
|
455
|
+
* refresh tokens, so keeping the previous grant around and using it again can
|
|
456
|
+
* invalidate the whole family.
|
|
457
|
+
*/
|
|
458
|
+
declare function refreshGrant(grant: Grant, fetchImpl?: FetchLike): Promise<Grant>;
|
|
459
|
+
/**
|
|
460
|
+
* The secrets held between building the consent URL and redeeming the code.
|
|
461
|
+
*
|
|
462
|
+
* {@link AuthCodeFlow.exchange} consumes it, so a verifier is not replayed
|
|
463
|
+
* against a second code.
|
|
464
|
+
*/
|
|
465
|
+
interface PendingAuthorization {
|
|
466
|
+
readonly codeVerifier: string;
|
|
467
|
+
readonly state: string;
|
|
468
|
+
readonly redirectUri: string;
|
|
469
|
+
}
|
|
470
|
+
/** Drives discovery, registration, consent, and exchange. */
|
|
471
|
+
declare class AuthCodeFlow {
|
|
472
|
+
private readonly fetchImpl;
|
|
473
|
+
private readonly metadataDoc;
|
|
474
|
+
/** The protected resource this grant will be bound to (RFC 8707). */
|
|
475
|
+
private readonly resource;
|
|
476
|
+
private clientIdValue?;
|
|
477
|
+
private redirectUriValue?;
|
|
478
|
+
private scope;
|
|
479
|
+
private constructor();
|
|
480
|
+
/**
|
|
481
|
+
* Discover the authorization server protecting `resourceUrl`.
|
|
482
|
+
*
|
|
483
|
+
* `resourceUrl` is the MCP endpoint being connected to — for DataGrout,
|
|
484
|
+
* `https://gateway.datagrout.ai/connect` or a `.../servers/{uuid}/mcp` URL.
|
|
485
|
+
*
|
|
486
|
+
* Tries RFC 9728 protected-resource metadata first, then RFC 8414
|
|
487
|
+
* authorization-server metadata on whatever that names. Falls back to the
|
|
488
|
+
* resource's own origin, which is where DataGrout serves it.
|
|
489
|
+
*/
|
|
490
|
+
static discover(resourceUrl: string, fetchImpl?: FetchLike): Promise<AuthCodeFlow>;
|
|
491
|
+
/** Use a client id registered out of band, skipping dynamic registration. */
|
|
492
|
+
withClientId(clientId: string, redirectUri: string): this;
|
|
493
|
+
/**
|
|
494
|
+
* Reuse a client registered on a previous run.
|
|
495
|
+
*
|
|
496
|
+
* Prefer this over {@link withClientId}: it carries the redirect URI with the
|
|
497
|
+
* id, which is not optional bookkeeping — an authorization server matches the
|
|
498
|
+
* redirect URI **exactly** against what was registered, so a client id reused
|
|
499
|
+
* with a different URI is rejected.
|
|
500
|
+
*/
|
|
501
|
+
withRegisteredClient(client: RegisteredClient): this;
|
|
502
|
+
/** Request scopes other than {@link DEFAULT_SCOPE}. */
|
|
503
|
+
withScope(scope: string): this;
|
|
504
|
+
/** The discovered metadata. */
|
|
505
|
+
get metadata(): AuthServerMetadata;
|
|
506
|
+
/** The client id, once registered or supplied. */
|
|
507
|
+
get clientId(): string | undefined;
|
|
508
|
+
/** The redirect URI this flow is bound to. */
|
|
509
|
+
get redirectUri(): string | undefined;
|
|
510
|
+
/**
|
|
511
|
+
* Register this application via RFC 7591 dynamic client registration.
|
|
512
|
+
*
|
|
513
|
+
* Registers a **public client** — `token_endpoint_auth_method: "none"`, no
|
|
514
|
+
* secret issued. A desktop or CLI application cannot keep a secret, and PKCE
|
|
515
|
+
* is what stands in for one.
|
|
516
|
+
*
|
|
517
|
+
* Returns the id **and** the redirect URI it is bound to. Persist the pair
|
|
518
|
+
* and restore it with {@link withRegisteredClient} — re-registering on every
|
|
519
|
+
* launch creates a new client record each time, and reusing an id against a
|
|
520
|
+
* different redirect URI is rejected.
|
|
521
|
+
*/
|
|
522
|
+
register(clientName: string, redirectUri: string): Promise<RegisteredClient>;
|
|
523
|
+
/**
|
|
524
|
+
* Build the consent URL, plus the {@link PendingAuthorization} needed to
|
|
525
|
+
* redeem the resulting code.
|
|
526
|
+
*
|
|
527
|
+
* The caller opens the URL however suits it — a browser, a printed
|
|
528
|
+
* instruction, a QR code. This SDK does not launch browsers.
|
|
529
|
+
*/
|
|
530
|
+
authorizeUrl(): {
|
|
531
|
+
url: string;
|
|
532
|
+
pending: PendingAuthorization;
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Redeem an authorization code for a {@link Grant}.
|
|
536
|
+
*
|
|
537
|
+
* `returnedState` is the `state` parameter from the redirect. It is checked
|
|
538
|
+
* against the pending request before anything is sent: a mismatch means the
|
|
539
|
+
* response belongs to a different authorization request, and the exchange is
|
|
540
|
+
* refused rather than attempted.
|
|
541
|
+
*/
|
|
542
|
+
exchange(pending: PendingAuthorization, code: string, returnedState: string): Promise<Grant>;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Holds a {@link Grant} and keeps its access token fresh.
|
|
546
|
+
*
|
|
547
|
+
* Mirrors {@link ./oauth.OAuthTokenProvider} so both grant types reach the
|
|
548
|
+
* transports through the same path — `getToken` on the way out, `invalidate`
|
|
549
|
+
* on a 401.
|
|
550
|
+
*/
|
|
551
|
+
declare class AuthCodeProvider {
|
|
552
|
+
private grantValue;
|
|
553
|
+
private dirty;
|
|
554
|
+
private refreshPromise;
|
|
555
|
+
private readonly fetchImpl;
|
|
556
|
+
constructor(grant: Grant, fetchImpl?: FetchLike);
|
|
557
|
+
/** The current access token, refreshing first if it is at or near expiry. */
|
|
558
|
+
getToken(): Promise<string>;
|
|
559
|
+
/** A snapshot of the current grant, for persisting. */
|
|
560
|
+
grant(): Grant;
|
|
561
|
+
/** Whether the grant changed since the last {@link takeIfDirty}. */
|
|
562
|
+
isDirty(): boolean;
|
|
563
|
+
/**
|
|
564
|
+
* Return the grant if it has changed since the last call, clearing the flag.
|
|
565
|
+
*
|
|
566
|
+
* The intended use is a persistence loop: call periodically and write
|
|
567
|
+
* whatever comes back, so a rotated refresh token is never lost.
|
|
568
|
+
*/
|
|
569
|
+
takeIfDirty(): Grant | null;
|
|
570
|
+
/** Force the next {@link getToken} to refresh. Call on a 401. */
|
|
571
|
+
invalidate(): void;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Coerce whatever `auth.authorizationCode` holds into a provider.
|
|
575
|
+
*
|
|
576
|
+
* A caller who passes a bare {@link Grant} gets one made for them; a caller who
|
|
577
|
+
* passes their own {@link AuthCodeProvider} keeps it, so a rotated refresh
|
|
578
|
+
* token stays visible to them through `takeIfDirty()`.
|
|
579
|
+
*/
|
|
580
|
+
declare function authCodeProviderFrom(value: Grant | AuthCodeProvider | undefined, fetchImpl?: FetchLike): AuthCodeProvider | undefined;
|
|
581
|
+
/** Generate an RFC 7636 code verifier: 43 characters of base64url. */
|
|
582
|
+
declare function generateVerifier(): string;
|
|
583
|
+
/** The S256 challenge for a verifier: `base64url(sha256(verifier))`. */
|
|
584
|
+
declare function challengeS256(verifier: string): string;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* OAuth 2.1 `client_credentials` token provider for Conduit.
|
|
588
|
+
*
|
|
589
|
+
* Fetches short-lived JWTs from the DataGrout machine-client token endpoint
|
|
590
|
+
* and caches them, refreshing proactively before they expire.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* // Most users should use the high-level `auth.clientCredentials` option on
|
|
595
|
+
* // ClientOptions instead of instantiating this class directly.
|
|
596
|
+
* import { Client } from 'datagrout-conduit';
|
|
597
|
+
*
|
|
598
|
+
* const client = new Client({
|
|
599
|
+
* url: 'https://app.datagrout.ai/servers/{uuid}/mcp',
|
|
600
|
+
* auth: {
|
|
601
|
+
* clientCredentials: { clientId: 'abc', clientSecret: 'xyz' },
|
|
602
|
+
* },
|
|
603
|
+
* });
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
/**
|
|
607
|
+
* Derive the token endpoint URL from a DataGrout MCP URL.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* deriveTokenEndpoint('https://app.datagrout.ai/servers/abc/mcp');
|
|
612
|
+
* // → 'https://app.datagrout.ai/servers/abc/oauth/token'
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
declare function deriveTokenEndpoint(mcpUrl: string): string;
|
|
616
|
+
/** @internal */
|
|
617
|
+
declare class OAuthTokenProvider {
|
|
618
|
+
private readonly clientId;
|
|
619
|
+
private readonly clientSecret;
|
|
620
|
+
private readonly tokenEndpoint;
|
|
621
|
+
private readonly scope?;
|
|
622
|
+
private cached;
|
|
623
|
+
private fetchPromise;
|
|
624
|
+
constructor(opts: {
|
|
625
|
+
clientId: string;
|
|
626
|
+
clientSecret: string;
|
|
627
|
+
tokenEndpoint: string;
|
|
628
|
+
scope?: string;
|
|
629
|
+
});
|
|
630
|
+
/**
|
|
631
|
+
* Return the current bearer token, fetching a fresh one if necessary.
|
|
632
|
+
* Concurrent callers share a single in-flight fetch.
|
|
633
|
+
*/
|
|
634
|
+
getToken(): Promise<string>;
|
|
635
|
+
/** Invalidate the cached token (e.g. after receiving a 401). */
|
|
636
|
+
invalidate(): void;
|
|
637
|
+
private fetchToken;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* RFC 8693 **delegation** — an agent acting *for* a user.
|
|
642
|
+
*
|
|
643
|
+
* The two grants this SDK already speaks each answer one question.
|
|
644
|
+
* {@link ./oauth} (`client_credentials`) says *which machine* is calling;
|
|
645
|
+
* {@link ./authcode} says *which person* consented. Neither says both, and an
|
|
646
|
+
* agent working on a user's behalf needs to: the resource server has to know
|
|
647
|
+
* whose data it is (`sub`) and who is actually holding the connection (`act`).
|
|
648
|
+
* The RFC 8693 exchange produces exactly that token, from two the caller
|
|
649
|
+
* already has.
|
|
650
|
+
*
|
|
651
|
+
* # Delegation, not impersonation
|
|
652
|
+
*
|
|
653
|
+
* RFC 8693 distinguishes the two. In **delegation** the issued token names the
|
|
654
|
+
* user as `sub` and the agent in an `act` claim, so the resource server can
|
|
655
|
+
* see — and audit, and rate-limit, and revoke — the agent separately from the
|
|
656
|
+
* user. In **impersonation** the agent simply *becomes* the user, and the
|
|
657
|
+
* resource server cannot tell the difference. DataGrout's authorization server
|
|
658
|
+
* issues delegation tokens and requires an `actor_token`; this module
|
|
659
|
+
* therefore **requires an actor by default** and refuses to build a request
|
|
660
|
+
* without one. Impersonation is an explicit opt-in via
|
|
661
|
+
* {@link DelegationRequest.impersonation}, for RFC 8693 servers that support
|
|
662
|
+
* it.
|
|
663
|
+
*
|
|
664
|
+
* # Wire contract
|
|
665
|
+
*
|
|
666
|
+
* `POST {tokenEndpoint}`, form-encoded, in this order:
|
|
667
|
+
*
|
|
668
|
+
* | field | value |
|
|
669
|
+
* |---|---|
|
|
670
|
+
* | `grant_type` | {@link GRANT_TYPE} |
|
|
671
|
+
* | `subject_token`, `subject_token_type` | the user's token and its {@link TokenType} URN |
|
|
672
|
+
* | `actor_token`, `actor_token_type` | the agent's token and URN — omitted only under `impersonation()` |
|
|
673
|
+
* | `client_id`, `client_secret?` | client authentication, in the body by default (see {@link ClientAuth}) |
|
|
674
|
+
* | `audience?`, `resource?`, `scope?`, `requested_token_type?` | as set |
|
|
675
|
+
*
|
|
676
|
+
* `resource` is RFC 8707 and, when set, is always sent — the same invariant the
|
|
677
|
+
* authorization-code module keeps, so a delegated token cannot be replayed
|
|
678
|
+
* against a different resource.
|
|
679
|
+
*
|
|
680
|
+
* **The client must be the actor.** The `client_id` authenticating the request
|
|
681
|
+
* and the principal behind `actor_token` are expected to be the same agent.
|
|
682
|
+
* This SDK does not verify that — it cannot, without decoding the actor token —
|
|
683
|
+
* and the server enforces it (`unauthorized_client` when they differ).
|
|
684
|
+
*
|
|
685
|
+
* The response is `{access_token, issued_token_type, token_type, expires_in?,
|
|
686
|
+
* scope?}`; errors are RFC 6749 bodies `{error, error_description?}`, with the
|
|
687
|
+
* codes listed in {@link SERVER_ERROR_CODES}.
|
|
688
|
+
*
|
|
689
|
+
* ```ts
|
|
690
|
+
* import {
|
|
691
|
+
* Client,
|
|
692
|
+
* DelegatedProvider,
|
|
693
|
+
* DelegationRequest,
|
|
694
|
+
* OAuthTokenProvider,
|
|
695
|
+
* TokenSource,
|
|
696
|
+
* TOKEN_TYPES,
|
|
697
|
+
* } from "@datagrout/conduit";
|
|
698
|
+
*
|
|
699
|
+
* // The agent's own credential — the actor.
|
|
700
|
+
* const agent = new OAuthTokenProvider({
|
|
701
|
+
* clientId: "agent_client_id",
|
|
702
|
+
* clientSecret: "agent_client_secret",
|
|
703
|
+
* tokenEndpoint: "https://gateway.datagrout.ai/oauth/token",
|
|
704
|
+
* });
|
|
705
|
+
*
|
|
706
|
+
* // The user's token — the subject. Here one handed to the agent for this run;
|
|
707
|
+
* // a long-lived app would use `TokenSource.authorizationCode(provider)`.
|
|
708
|
+
* const user = TokenSource.staticToken(userToken, TOKEN_TYPES.access_token);
|
|
709
|
+
*
|
|
710
|
+
* const request = new DelegationRequest(
|
|
711
|
+
* "https://gateway.datagrout.ai/oauth/token",
|
|
712
|
+
* "agent_client_id",
|
|
713
|
+
* )
|
|
714
|
+
* .clientSecret("agent_client_secret")
|
|
715
|
+
* .resource("https://gateway.datagrout.ai/connect");
|
|
716
|
+
*
|
|
717
|
+
* const provider = new DelegatedProvider(
|
|
718
|
+
* request,
|
|
719
|
+
* user,
|
|
720
|
+
* TokenSource.clientCredentials(agent),
|
|
721
|
+
* );
|
|
722
|
+
*
|
|
723
|
+
* const client = new Client({
|
|
724
|
+
* url: "https://gateway.datagrout.ai/connect",
|
|
725
|
+
* auth: { delegation: provider },
|
|
726
|
+
* });
|
|
727
|
+
* ```
|
|
728
|
+
*
|
|
729
|
+
* # Naming
|
|
730
|
+
*
|
|
731
|
+
* Elsewhere in this SDK "token exchange" already means redeeming a
|
|
732
|
+
* `client_credentials` grant — `AuthCodeError`'s `token_exchange` kind, and the
|
|
733
|
+
* onramp's `token_exchange` stage. This module says *delegation* and *exchange*
|
|
734
|
+
* — {@link DelegationRequest.exchange}, {@link DelegatedToken} — and never
|
|
735
|
+
* reuses that label, so a log line cannot be read two ways.
|
|
736
|
+
*/
|
|
737
|
+
|
|
738
|
+
/** The RFC 8693 grant type. */
|
|
739
|
+
declare const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
740
|
+
/**
|
|
741
|
+
* `util.inspect`'s hook, resolved once so it can key a class method.
|
|
742
|
+
*
|
|
743
|
+
* Both this module's inspect overrides exist to keep tokens and the client
|
|
744
|
+
* secret out of a debug dump: without one, `util.inspect` walks the instance's
|
|
745
|
+
* own fields and prints them.
|
|
746
|
+
*/
|
|
747
|
+
declare const INSPECT_CUSTOM: unique symbol;
|
|
748
|
+
/**
|
|
749
|
+
* RFC 6749 error codes an RFC 8693 endpoint returns, as
|
|
750
|
+
* {@link DelegationError.error} on a `server` failure.
|
|
751
|
+
*
|
|
752
|
+
* Listed so callers and ports compare against a name rather than a string they
|
|
753
|
+
* typed. `invalid_target` is the one specific to RFC 8693: the `audience` or
|
|
754
|
+
* `resource` is not one this server issues tokens for.
|
|
755
|
+
*/
|
|
756
|
+
declare const SERVER_ERROR_CODES: readonly ["invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "invalid_target", "invalid_scope", "unsupported_grant_type"];
|
|
757
|
+
/** One of {@link SERVER_ERROR_CODES}. */
|
|
758
|
+
type ServerErrorCode = (typeof SERVER_ERROR_CODES)[number];
|
|
759
|
+
/**
|
|
760
|
+
* The RFC 8693 §3 token-type URNs this SDK names.
|
|
761
|
+
*
|
|
762
|
+
* Keyed by the RFC's own short names — which are also the keys
|
|
763
|
+
* `testdata/contract.json` uses — rather than camelCase, so the fixture and the
|
|
764
|
+
* table are read side by side.
|
|
765
|
+
*/
|
|
766
|
+
declare const TOKEN_TYPES: Readonly<{
|
|
767
|
+
/** The default for both subject and actor, and what DataGrout issues. */
|
|
768
|
+
readonly access_token: "urn:ietf:params:oauth:token-type:access_token";
|
|
769
|
+
/** A JWT presented as a JWT rather than as an opaque access token. */
|
|
770
|
+
readonly jwt: "urn:ietf:params:oauth:token-type:jwt";
|
|
771
|
+
readonly id_token: "urn:ietf:params:oauth:token-type:id_token";
|
|
772
|
+
readonly refresh_token: "urn:ietf:params:oauth:token-type:refresh_token";
|
|
773
|
+
readonly saml2: "urn:ietf:params:oauth:token-type:saml2";
|
|
774
|
+
}>;
|
|
775
|
+
/** A URN this SDK names. */
|
|
776
|
+
type NamedTokenTypeUrn = (typeof TOKEN_TYPES)[keyof typeof TOKEN_TYPES];
|
|
777
|
+
/**
|
|
778
|
+
* An RFC 8693 §3 token type identifier, **as its URN**.
|
|
779
|
+
*
|
|
780
|
+
* A token type *is* its URN here, so the wire shape is the same string in every
|
|
781
|
+
* language and serialization is the identity: nothing has to be mapped on the
|
|
782
|
+
* way out or in. The five named URNs autocomplete via {@link TOKEN_TYPES}; any
|
|
783
|
+
* other URN is carried verbatim, which is the open `other` case Rust models as
|
|
784
|
+
* `TokenType::Other` — {@link tokenTypeName} reports it as `"other"`.
|
|
785
|
+
*/
|
|
786
|
+
type TokenType = NamedTokenTypeUrn | (string & {});
|
|
787
|
+
/** The short name of a token type, or `"other"` for a URN this SDK does not name. */
|
|
788
|
+
type TokenTypeName = keyof typeof TOKEN_TYPES | "other";
|
|
789
|
+
/**
|
|
790
|
+
* The short name for a token-type URN.
|
|
791
|
+
*
|
|
792
|
+
* For logs, tests and cross-language comparison — the value itself stays the
|
|
793
|
+
* URN, so this is never needed to build a request.
|
|
794
|
+
*/
|
|
795
|
+
declare function tokenTypeName(tokenType: TokenType): TokenTypeName;
|
|
796
|
+
/**
|
|
797
|
+
* The distinguishable failures of a delegation exchange.
|
|
798
|
+
*
|
|
799
|
+
* The taxonomy is part of the cross-language contract: every conduit SDK
|
|
800
|
+
* distinguishes these same cases under the same names, so callers can branch
|
|
801
|
+
* identically.
|
|
802
|
+
*/
|
|
803
|
+
type DelegationErrorKind =
|
|
804
|
+
/** No subject token was set — there is nobody to act for. */
|
|
805
|
+
"missing_subject"
|
|
806
|
+
/** No actor token was set and the request is not an impersonation. */
|
|
807
|
+
| "missing_actor"
|
|
808
|
+
/** Transport failure talking to the token endpoint. */
|
|
809
|
+
| "http"
|
|
810
|
+
/** The token endpoint refused, with an RFC 6749 error body. */
|
|
811
|
+
| "server"
|
|
812
|
+
/**
|
|
813
|
+
* The endpoint answered with something that is not an RFC 8693 response — a
|
|
814
|
+
* success body missing required fields, or a failure whose body is not an
|
|
815
|
+
* RFC 6749 error.
|
|
816
|
+
*/
|
|
817
|
+
| "invalid_response";
|
|
818
|
+
/** An error from a delegation exchange, tagged with its {@link DelegationErrorKind}. */
|
|
819
|
+
declare class DelegationError extends ConduitError {
|
|
820
|
+
readonly kind: DelegationErrorKind;
|
|
821
|
+
/** HTTP status, for `server`. */
|
|
822
|
+
readonly status?: number;
|
|
823
|
+
/** RFC 6749 error code, for `server`; see {@link SERVER_ERROR_CODES}. */
|
|
824
|
+
readonly error?: ServerErrorCode | (string & {});
|
|
825
|
+
/** Human-readable description, when the server gave one. */
|
|
826
|
+
readonly errorDescription?: string;
|
|
827
|
+
constructor(kind: DelegationErrorKind, message: string, extra?: {
|
|
828
|
+
status?: number;
|
|
829
|
+
error?: ServerErrorCode | (string & {});
|
|
830
|
+
errorDescription?: string;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* How the client authenticates to the token endpoint.
|
|
835
|
+
*
|
|
836
|
+
* - `"body"` — `client_id` and `client_secret` as form fields (RFC 6749 §2.3.1
|
|
837
|
+
* `client_secret_post`). The default, and what DataGrout expects.
|
|
838
|
+
* - `"basic"` — `Authorization: Basic base64(client_id:client_secret)`
|
|
839
|
+
* (`client_secret_basic`). `client_id` is still sent in the body, as RFC 6749
|
|
840
|
+
* permits and some servers require.
|
|
841
|
+
*/
|
|
842
|
+
type ClientAuth = "body" | "basic";
|
|
843
|
+
/**
|
|
844
|
+
* A delegation request, built up and then {@link DelegationRequest.exchange}d.
|
|
845
|
+
*
|
|
846
|
+
* The builder methods mutate and return `this`, as {@link AuthCodeFlow}'s do.
|
|
847
|
+
* {@link DelegationRequest.clone} exists because a {@link DelegatedProvider}
|
|
848
|
+
* holds one as a template and fills in fresh subject and actor tokens on each
|
|
849
|
+
* re-exchange.
|
|
850
|
+
*/
|
|
851
|
+
declare class DelegationRequest {
|
|
852
|
+
private readonly endpoint;
|
|
853
|
+
private readonly client;
|
|
854
|
+
private secret?;
|
|
855
|
+
private auth;
|
|
856
|
+
private subject?;
|
|
857
|
+
private actor?;
|
|
858
|
+
private audienceValue?;
|
|
859
|
+
private resourceValue?;
|
|
860
|
+
private scopeValue?;
|
|
861
|
+
private requestedTokenTypeValue?;
|
|
862
|
+
private impersonating;
|
|
863
|
+
/**
|
|
864
|
+
* Start a request against `tokenEndpoint`, authenticating as `clientId`.
|
|
865
|
+
*
|
|
866
|
+
* The client should be the actor — see the module docs.
|
|
867
|
+
*/
|
|
868
|
+
constructor(tokenEndpoint: string, clientId: string);
|
|
869
|
+
/** The client secret, for confidential clients. */
|
|
870
|
+
clientSecret(secret: string): this;
|
|
871
|
+
/** Where the client secret travels. Defaults to `"body"`. */
|
|
872
|
+
clientAuth(auth: ClientAuth): this;
|
|
873
|
+
/**
|
|
874
|
+
* The token being exchanged: the **user's**, whose identity the issued token
|
|
875
|
+
* will carry as `sub`.
|
|
876
|
+
*/
|
|
877
|
+
subjectToken(token: string, tokenType?: TokenType): this;
|
|
878
|
+
/** The **agent's** own token, which the issued token will name in `act`. */
|
|
879
|
+
actorToken(token: string, tokenType?: TokenType): this;
|
|
880
|
+
/** Logical name of the service the token is for (RFC 8693 `audience`). */
|
|
881
|
+
audience(audience: string): this;
|
|
882
|
+
/**
|
|
883
|
+
* URI of the resource the token is for (RFC 8707 `resource`). Always sent
|
|
884
|
+
* when set, so the token cannot be replayed elsewhere.
|
|
885
|
+
*/
|
|
886
|
+
resource(resource: string): this;
|
|
887
|
+
/** Scopes to request, space-separated. */
|
|
888
|
+
scope(scope: string): this;
|
|
889
|
+
/** The kind of token wanted back. Servers default to an access token. */
|
|
890
|
+
requestedTokenType(tokenType: TokenType): this;
|
|
891
|
+
/**
|
|
892
|
+
* Opt out of delegation: send no `actor_token`, so the issued token has no
|
|
893
|
+
* `act` claim and the agent is indistinguishable from the user.
|
|
894
|
+
*
|
|
895
|
+
* DataGrout does not issue these. This exists for other RFC 8693 servers, and
|
|
896
|
+
* it is a builder call rather than a default precisely so that forgetting to
|
|
897
|
+
* set an actor is an error instead of a silent downgrade.
|
|
898
|
+
*/
|
|
899
|
+
impersonation(): this;
|
|
900
|
+
/** The token endpoint this request posts to. */
|
|
901
|
+
get tokenEndpoint(): string;
|
|
902
|
+
/** The client id this request authenticates as. */
|
|
903
|
+
get clientId(): string;
|
|
904
|
+
/** Whether {@link impersonation} was called. */
|
|
905
|
+
get isImpersonation(): boolean;
|
|
906
|
+
/** An independent copy, so a template can be filled in per exchange. */
|
|
907
|
+
clone(): DelegationRequest;
|
|
908
|
+
/**
|
|
909
|
+
* The form body this request will post, in wire order.
|
|
910
|
+
*
|
|
911
|
+
* Throws before any network activity when the request is incomplete:
|
|
912
|
+
* `missing_subject`, or `missing_actor` unless {@link impersonation} was
|
|
913
|
+
* called. Public so a caller — or another SDK's test suite — can check the
|
|
914
|
+
* body against the contract fixture without a server.
|
|
915
|
+
*/
|
|
916
|
+
formParams(): Array<[string, string]>;
|
|
917
|
+
/** Perform the exchange. */
|
|
918
|
+
exchange(fetchImpl?: FetchLike): Promise<DelegatedToken>;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* A token issued by an exchange.
|
|
922
|
+
*
|
|
923
|
+
* The serialized shape is part of the cross-language contract, and is what
|
|
924
|
+
* `testdata/contract.json` pins: `access_token`, `issued_token_type` (a URN
|
|
925
|
+
* string), `token_type`, `expires_at?`, `scope?`. Field names are therefore
|
|
926
|
+
* snake_case and fixed, so `JSON.stringify` on one of these is the wire form —
|
|
927
|
+
* exactly as for `Grant`. As there, `expires_at` is **Unix seconds** —
|
|
928
|
+
* computed from the server's relative `expires_in` at receipt — never
|
|
929
|
+
* milliseconds and never a monotonic clock reading, so the token means the same
|
|
930
|
+
* thing once written down.
|
|
931
|
+
*/
|
|
932
|
+
interface DelegatedToken {
|
|
933
|
+
/** The bearer token to present. */
|
|
934
|
+
access_token: string;
|
|
935
|
+
/** What kind of token was issued, as its {@link TokenType} URN. */
|
|
936
|
+
issued_token_type: TokenType;
|
|
937
|
+
/** How to present it — `Bearer`, in practice. */
|
|
938
|
+
token_type: string;
|
|
939
|
+
/** Absolute expiry, Unix **seconds**. Absent means the server did not say. */
|
|
940
|
+
expires_at?: number;
|
|
941
|
+
/** Granted scopes, when the server reported them. */
|
|
942
|
+
scope?: string;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* True when the token is expired, or within the refresh skew of it.
|
|
946
|
+
*
|
|
947
|
+
* A token with no stated expiry is treated as live: the server chose not to say,
|
|
948
|
+
* and guessing would throw away working tokens.
|
|
949
|
+
*/
|
|
950
|
+
declare function isDelegatedTokenExpired(token: DelegatedToken): boolean;
|
|
951
|
+
/** What a {@link TokenSource} draws its token from — for logs, never the token. */
|
|
952
|
+
type TokenSourceKind = "static" | "client_credentials" | "authorization_code" | "dynamic";
|
|
953
|
+
/**
|
|
954
|
+
* Where a {@link DelegatedProvider} gets a subject or actor token from, and what
|
|
955
|
+
* {@link TokenType} to declare it as.
|
|
956
|
+
*
|
|
957
|
+
* A source is consulted on **every** exchange, so a provider-backed source hands
|
|
958
|
+
* over a *fresh* token each time — the whole point of wrapping a provider rather
|
|
959
|
+
* than copying its current token out.
|
|
960
|
+
*/
|
|
961
|
+
declare class TokenSource {
|
|
962
|
+
private readonly resolver;
|
|
963
|
+
private readonly sourceKind;
|
|
964
|
+
private readonly declaredType;
|
|
965
|
+
private constructor();
|
|
966
|
+
/** A fixed token, e.g. one handed to the agent for this run. */
|
|
967
|
+
static staticToken(token: string, tokenType?: TokenType): TokenSource;
|
|
968
|
+
/** The agent's own `client_credentials` provider — the usual **actor**. */
|
|
969
|
+
static clientCredentials(provider: OAuthTokenProvider, tokenType?: TokenType): TokenSource;
|
|
970
|
+
/**
|
|
971
|
+
* A user's authorization-code provider — the usual **subject** in an app that
|
|
972
|
+
* signed the user in itself. Refreshes its grant as needed, so the exchange
|
|
973
|
+
* always sees a live subject token.
|
|
974
|
+
*/
|
|
975
|
+
static authorizationCode(provider: AuthCodeProvider, tokenType?: TokenType): TokenSource;
|
|
976
|
+
/**
|
|
977
|
+
* Any function that yields a token — a vault lookup, a header from an inbound
|
|
978
|
+
* request, another SDK's provider. Called on every exchange.
|
|
979
|
+
*/
|
|
980
|
+
static dynamic(fn: () => string | Promise<string>, tokenType?: TokenType): TokenSource;
|
|
981
|
+
/** Declare a different {@link TokenType} for this source. */
|
|
982
|
+
withTokenType(tokenType: TokenType): TokenSource;
|
|
983
|
+
/** The declared token type. */
|
|
984
|
+
get tokenType(): TokenType;
|
|
985
|
+
/** Where the token comes from. */
|
|
986
|
+
get kind(): TokenSourceKind;
|
|
987
|
+
/** Draw a token. */
|
|
988
|
+
resolve(): Promise<string>;
|
|
989
|
+
/** Never print tokens — only where they come from. */
|
|
990
|
+
toJSON(): {
|
|
991
|
+
kind: TokenSourceKind;
|
|
992
|
+
tokenType: TokenType;
|
|
993
|
+
};
|
|
994
|
+
[INSPECT_CUSTOM](): string;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Keeps a delegated token fresh, re-exchanging when it nears expiry.
|
|
998
|
+
*
|
|
999
|
+
* The third token provider in this SDK, shaped like the other two —
|
|
1000
|
+
* {@link OAuthTokenProvider} and {@link AuthCodeProvider} — so every transport
|
|
1001
|
+
* reaches it through the same path: `getToken` on the way out, `invalidate` on a
|
|
1002
|
+
* 401. Each exchange pulls a fresh subject and actor token from its
|
|
1003
|
+
* {@link TokenSource}s, so an expiring upstream credential is handled by the
|
|
1004
|
+
* provider that owns it.
|
|
1005
|
+
*/
|
|
1006
|
+
declare class DelegatedProvider {
|
|
1007
|
+
private readonly template;
|
|
1008
|
+
private readonly subject;
|
|
1009
|
+
private readonly actor?;
|
|
1010
|
+
private readonly fetchImpl;
|
|
1011
|
+
private cached;
|
|
1012
|
+
/**
|
|
1013
|
+
* The in-flight exchange, so concurrent callers make one request rather than
|
|
1014
|
+
* a stampede. Mirrors `AuthCodeProvider`'s refresh de-duplication.
|
|
1015
|
+
*/
|
|
1016
|
+
private exchangePromise;
|
|
1017
|
+
/**
|
|
1018
|
+
* Wrap a request template with the sources of its two tokens.
|
|
1019
|
+
*
|
|
1020
|
+
* Any `subjectToken` or `actorToken` already on `request` is ignored; the
|
|
1021
|
+
* sources supply them. Omit `actor` only with a request that called
|
|
1022
|
+
* {@link DelegationRequest.impersonation} — otherwise every `getToken` fails
|
|
1023
|
+
* with `missing_actor`, which is the intended loud failure rather than a
|
|
1024
|
+
* silent downgrade.
|
|
1025
|
+
*/
|
|
1026
|
+
constructor(request: DelegationRequest, subject: TokenSource, actor?: TokenSource, fetchImpl?: FetchLike);
|
|
1027
|
+
/**
|
|
1028
|
+
* The current delegated bearer, exchanging first if there is none or it is at
|
|
1029
|
+
* or near expiry.
|
|
1030
|
+
*/
|
|
1031
|
+
getToken(): Promise<string>;
|
|
1032
|
+
/**
|
|
1033
|
+
* Force the next {@link getToken} to exchange again. Call on a 401.
|
|
1034
|
+
*
|
|
1035
|
+
* Only the delegated token is dropped. The subject and actor sources are left
|
|
1036
|
+
* alone: a provider-backed source tracks its own expiry, and a 401 from the
|
|
1037
|
+
* resource server says nothing about them.
|
|
1038
|
+
*/
|
|
1039
|
+
invalidate(): void;
|
|
1040
|
+
/** A snapshot of the cached token, if any — for inspection or logging. */
|
|
1041
|
+
token(): DelegatedToken | undefined;
|
|
1042
|
+
/** The request template, without tokens. */
|
|
1043
|
+
get request(): DelegationRequest;
|
|
1044
|
+
/** Never print tokens, and never the client secret the template carries. */
|
|
1045
|
+
toJSON(): Record<string, unknown>;
|
|
1046
|
+
[INSPECT_CUSTOM](): string;
|
|
1047
|
+
private liveToken;
|
|
1048
|
+
private exchange;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
220
1051
|
/**
|
|
221
1052
|
* Type definitions for DataGrout Conduit
|
|
222
1053
|
*/
|
|
@@ -371,6 +1202,29 @@ interface AuthConfig {
|
|
|
371
1202
|
/** Optional space-separated scope string (e.g. `"mcp tools"`). */
|
|
372
1203
|
scope?: string;
|
|
373
1204
|
};
|
|
1205
|
+
/**
|
|
1206
|
+
* OAuth 2.1 **authorization code** grant — a signed-in *person* rather than
|
|
1207
|
+
* a machine. Obtain one with `AuthCodeFlow`, persist it, and pass it here on
|
|
1208
|
+
* later runs.
|
|
1209
|
+
*
|
|
1210
|
+
* Pass a `Grant` and the SDK wraps it. Pass an `AuthCodeProvider` to keep
|
|
1211
|
+
* ownership, which is what you want when a rotated refresh token has to be
|
|
1212
|
+
* written back: poll `takeIfDirty()` and persist whatever it returns.
|
|
1213
|
+
*/
|
|
1214
|
+
authorizationCode?: Grant | AuthCodeProvider;
|
|
1215
|
+
/**
|
|
1216
|
+
* RFC 8693 **delegation** — an agent acting *for* a user, so the issued token
|
|
1217
|
+
* names the user as `sub` and the agent in `act`.
|
|
1218
|
+
*
|
|
1219
|
+
* `clientCredentials` says which machine is calling and `authorizationCode`
|
|
1220
|
+
* says which person consented; a delegated token says both. Build a
|
|
1221
|
+
* `DelegatedProvider` (see `./delegation`) and pass it here: the transports
|
|
1222
|
+
* exchange on the way out and re-exchange on a 401, exactly as they do for the
|
|
1223
|
+
* other two grants.
|
|
1224
|
+
*
|
|
1225
|
+
* Set alongside another option it wins, being the most specific choice.
|
|
1226
|
+
*/
|
|
1227
|
+
delegation?: DelegatedProvider;
|
|
374
1228
|
custom?: Record<string, string>;
|
|
375
1229
|
}
|
|
376
1230
|
interface ClientOptions {
|
|
@@ -644,6 +1498,23 @@ declare class Subscription {
|
|
|
644
1498
|
declare class WsTransport extends Transport {
|
|
645
1499
|
private readonly _url;
|
|
646
1500
|
private readonly _auth?;
|
|
1501
|
+
/**
|
|
1502
|
+
* mTLS identity presented on the `wss://` handshake, if any. The HTTP
|
|
1503
|
+
* transports route through {@link fetchWithIdentity}; here the PEMs go to the
|
|
1504
|
+
* `ws` client as `cert` / `key` / `ca` options, which it forwards to
|
|
1505
|
+
* `tls.connect`. Mirrors `build_connector` in the Rust reference.
|
|
1506
|
+
*/
|
|
1507
|
+
private readonly _identity?;
|
|
1508
|
+
/**
|
|
1509
|
+
* Resolved OAuth providers, built once so a token survives reconnects.
|
|
1510
|
+
*
|
|
1511
|
+
* All are consulted in {@link _resolveBearer} before the upgrade request is
|
|
1512
|
+
* built — see the note there on why that has to happen up front.
|
|
1513
|
+
*/
|
|
1514
|
+
private readonly _oauthProvider?;
|
|
1515
|
+
private readonly _authCodeProvider?;
|
|
1516
|
+
/** RFC 8693 delegation, when `auth.delegation` is set. */
|
|
1517
|
+
private readonly _delegatedProvider?;
|
|
647
1518
|
private _ws;
|
|
648
1519
|
private _nextId;
|
|
649
1520
|
private readonly _pending;
|
|
@@ -660,7 +1531,17 @@ declare class WsTransport extends Transport {
|
|
|
660
1531
|
* defaults to {@link PING_INTERVAL_MS}.
|
|
661
1532
|
*/
|
|
662
1533
|
private _pingIntervalMs;
|
|
663
|
-
constructor(url: string, auth?: AuthConfig, _timeout?: number,
|
|
1534
|
+
constructor(url: string, auth?: AuthConfig, _timeout?: number, identity?: ConduitIdentity);
|
|
1535
|
+
/**
|
|
1536
|
+
* The bearer to put on the upgrade request, if any.
|
|
1537
|
+
*
|
|
1538
|
+
* Resolved *before* the handshake is built. Fetching a token is async while
|
|
1539
|
+
* header construction is not, so a provider-backed token could never reach
|
|
1540
|
+
* the upgrade if it were resolved inside the header builder — which is
|
|
1541
|
+
* exactly the bug this replaced: an OAuth client authenticated over WS only
|
|
1542
|
+
* if it also happened to present an mTLS identity.
|
|
1543
|
+
*/
|
|
1544
|
+
private _resolveBearer;
|
|
664
1545
|
connect(): Promise<void>;
|
|
665
1546
|
disconnect(): Promise<void>;
|
|
666
1547
|
/**
|
|
@@ -1253,57 +2134,93 @@ declare class Client {
|
|
|
1253
2134
|
}
|
|
1254
2135
|
|
|
1255
2136
|
/**
|
|
1256
|
-
*
|
|
2137
|
+
* Capture the OAuth redirect on `127.0.0.1`.
|
|
1257
2138
|
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
2139
|
+
* A native app has no web server to redirect to, so it runs one for a few
|
|
2140
|
+
* seconds: bind a loopback port, send the user to the consent page, and read
|
|
2141
|
+
* the `code` off the single request the browser makes coming back.
|
|
2142
|
+
*
|
|
2143
|
+
* This lives in its own module rather than in `authcode` so a headless caller
|
|
2144
|
+
* can take the flow without pulling in an HTTP server — the same split every
|
|
2145
|
+
* conduit SDK makes, so the surface looks the same in every language.
|
|
1260
2146
|
*
|
|
1261
|
-
* @example
|
|
1262
2147
|
* ```ts
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
* import { Client } from 'datagrout-conduit';
|
|
2148
|
+
* import { AuthCodeFlow } from "@datagrout/conduit";
|
|
2149
|
+
* import { LoopbackListener } from "@datagrout/conduit";
|
|
1266
2150
|
*
|
|
1267
|
-
* const
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
* clientCredentials: { clientId: 'abc', clientSecret: 'xyz' },
|
|
1271
|
-
* },
|
|
1272
|
-
* });
|
|
1273
|
-
* ```
|
|
1274
|
-
*/
|
|
1275
|
-
/**
|
|
1276
|
-
* Derive the token endpoint URL from a DataGrout MCP URL.
|
|
2151
|
+
* const listener = await LoopbackListener.bind();
|
|
2152
|
+
* const flow = await AuthCodeFlow.discover("https://gateway.datagrout.ai/connect");
|
|
2153
|
+
* await flow.register("My App", listener.redirectUri);
|
|
1277
2154
|
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
2155
|
+
* const { url, pending } = flow.authorizeUrl();
|
|
2156
|
+
* // open `url` in a browser however suits the application
|
|
2157
|
+
*
|
|
2158
|
+
* const redirect = await listener.wait(300_000);
|
|
2159
|
+
* const grant = await flow.exchange(pending, redirect.code, redirect.state);
|
|
1282
2160
|
* ```
|
|
1283
2161
|
*/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
});
|
|
2162
|
+
/** What the authorization server sent back to the redirect URI. */
|
|
2163
|
+
interface Redirect {
|
|
2164
|
+
/** The authorization code. */
|
|
2165
|
+
code: string;
|
|
2166
|
+
/** The `state` parameter, to be checked against the pending request. */
|
|
2167
|
+
state: string;
|
|
2168
|
+
}
|
|
2169
|
+
/** A one-shot loopback listener for the OAuth redirect. */
|
|
2170
|
+
declare class LoopbackListener {
|
|
2171
|
+
private readonly server;
|
|
2172
|
+
private readonly boundPort;
|
|
2173
|
+
private readonly path;
|
|
2174
|
+
private settled;
|
|
2175
|
+
private constructor();
|
|
1299
2176
|
/**
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
2177
|
+
* Bind an OS-assigned port on `127.0.0.1`.
|
|
2178
|
+
*
|
|
2179
|
+
* Letting the OS choose avoids fighting whatever else owns a fixed port —
|
|
2180
|
+
* and because registration happens after binding, the real port is already
|
|
2181
|
+
* known by the time the redirect URI is registered.
|
|
1302
2182
|
*/
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
|
|
1306
|
-
|
|
2183
|
+
static bind(): Promise<LoopbackListener>;
|
|
2184
|
+
/**
|
|
2185
|
+
* Bind a specific port and path.
|
|
2186
|
+
*
|
|
2187
|
+
* Use when the client was registered out of band against a fixed redirect
|
|
2188
|
+
* URI and the authorization server will accept no other.
|
|
2189
|
+
*/
|
|
2190
|
+
static bindOn(port: number, path: string): Promise<LoopbackListener>;
|
|
2191
|
+
/**
|
|
2192
|
+
* Re-bind the exact port and path of a previously registered redirect URI.
|
|
2193
|
+
*
|
|
2194
|
+
* Needed whenever a saved registration is reused: the authorization server
|
|
2195
|
+
* matches the redirect URI exactly, so the listener has to come back on the
|
|
2196
|
+
* same port it registered.
|
|
2197
|
+
*
|
|
2198
|
+
* Rejects if that port is occupied. The right recovery is to {@link bind} a
|
|
2199
|
+
* fresh port and register a new client — not to retry, and not to authorize
|
|
2200
|
+
* against a URI the server will reject.
|
|
2201
|
+
*/
|
|
2202
|
+
static bindFor(redirectUri: string): Promise<LoopbackListener>;
|
|
2203
|
+
/** The port actually bound. */
|
|
2204
|
+
get port(): number;
|
|
2205
|
+
/**
|
|
2206
|
+
* The redirect URI to register and to send in the authorize request.
|
|
2207
|
+
*
|
|
2208
|
+
* Uses `127.0.0.1` rather than `localhost`: RFC 8252 recommends the literal
|
|
2209
|
+
* address, and it sidesteps hosts where `localhost` resolves to IPv6 first
|
|
2210
|
+
* while the listener is bound to IPv4.
|
|
2211
|
+
*/
|
|
2212
|
+
get redirectUri(): string;
|
|
2213
|
+
/** Stop listening. Safe to call more than once. */
|
|
2214
|
+
close(): void;
|
|
2215
|
+
/**
|
|
2216
|
+
* Wait for the browser's redirect, up to `timeoutMs`.
|
|
2217
|
+
*
|
|
2218
|
+
* Serves a small page either way so the user sees an outcome rather than a
|
|
2219
|
+
* browser error, then stops listening. Requests to other paths are answered
|
|
2220
|
+
* 404 and ignored — browsers routinely ask for `/favicon.ico`, and treating
|
|
2221
|
+
* that as the redirect would abort the flow.
|
|
2222
|
+
*/
|
|
2223
|
+
wait(timeoutMs: number): Promise<Redirect>;
|
|
1307
2224
|
}
|
|
1308
2225
|
|
|
1309
2226
|
/**
|
|
@@ -1456,79 +2373,13 @@ interface SavedPaths {
|
|
|
1456
2373
|
declare function saveIdentity(identity: RegisteredIdentity, directory?: string): SavedPaths;
|
|
1457
2374
|
|
|
1458
2375
|
/**
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
* All errors extend `ConduitError` so callers can catch the whole family with
|
|
1462
|
-
* a single `instanceof ConduitError` check, or target specific subclasses.
|
|
1463
|
-
*/
|
|
1464
|
-
|
|
1465
|
-
/**
|
|
1466
|
-
* Base class for all errors thrown by the Conduit SDK.
|
|
1467
|
-
*
|
|
1468
|
-
* Fixes the prototype chain so `instanceof` works correctly when compiling
|
|
1469
|
-
* to CommonJS / ES5 targets.
|
|
1470
|
-
*/
|
|
1471
|
-
declare class ConduitError extends Error {
|
|
1472
|
-
constructor(message: string);
|
|
1473
|
-
}
|
|
1474
|
-
/**
|
|
1475
|
-
* Thrown when a `Client` method is called before `connect()` has been invoked,
|
|
1476
|
-
* or after `disconnect()` has been called.
|
|
1477
|
-
*/
|
|
1478
|
-
declare class NotInitializedError extends ConduitError {
|
|
1479
|
-
constructor();
|
|
1480
|
-
}
|
|
1481
|
-
/**
|
|
1482
|
-
* Thrown when the DataGrout gateway returns HTTP 429 (Too Many Requests).
|
|
1483
|
-
*
|
|
1484
|
-
* Authenticated DataGrout users are never rate-limited. Unauthenticated
|
|
1485
|
-
* callers hitting the hourly cap will receive this error.
|
|
1486
|
-
*
|
|
1487
|
-
* @property status - Parsed rate-limit header state.
|
|
1488
|
-
* @property retryAfter - Seconds to wait before retrying (from `Retry-After` header), if present.
|
|
1489
|
-
*/
|
|
1490
|
-
declare class RateLimitError extends ConduitError {
|
|
1491
|
-
readonly status: RateLimitStatus;
|
|
1492
|
-
readonly retryAfter?: number;
|
|
1493
|
-
constructor(status: RateLimitStatus, retryAfter?: number);
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Thrown when the server returns HTTP 401 Unauthorized or HTTP 403 Forbidden.
|
|
1497
|
-
*/
|
|
1498
|
-
declare class AuthError extends ConduitError {
|
|
1499
|
-
constructor(message?: string);
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Thrown on network-level failures such as fetch errors, connection refused,
|
|
1503
|
-
* or request timeouts.
|
|
1504
|
-
*/
|
|
1505
|
-
declare class NetworkError extends ConduitError {
|
|
1506
|
-
constructor(message: string);
|
|
1507
|
-
}
|
|
1508
|
-
/**
|
|
1509
|
-
* Thrown when the server returns an unexpected non-success HTTP status or
|
|
1510
|
-
* a JSON-RPC error payload.
|
|
2376
|
+
* The package version, in one place.
|
|
1511
2377
|
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
readonly code: number;
|
|
1517
|
-
readonly serverMessage: string;
|
|
1518
|
-
constructor(code: number, serverMessage: string);
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Thrown when required parameters are missing, mutually-exclusive option
|
|
1522
|
-
* combinations are invalid, or a method receives an unusable configuration.
|
|
2378
|
+
* A leaf module so anything can read it — including the transports, which
|
|
2379
|
+
* `index.ts` re-exports and therefore cannot be imported *from* without a
|
|
2380
|
+
* cycle. The MCP handshake reports this to the server, and it used to be a
|
|
2381
|
+
* separate hardcoded literal that had drifted six minor versions behind.
|
|
1523
2382
|
*/
|
|
1524
|
-
declare
|
|
1525
|
-
constructor(message: string);
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
/**
|
|
1529
|
-
* DataGrout Conduit SDK for TypeScript/JavaScript
|
|
1530
|
-
*/
|
|
1531
|
-
|
|
1532
|
-
declare const version = "0.5.0";
|
|
2383
|
+
declare const version = "0.8.0";
|
|
1533
2384
|
|
|
1534
|
-
export { type AuthConfig, AuthError, type Byok, type ChartOptions, Client, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, NetworkError, NotInitializedError, OAuthTokenProvider, type OnrampCredentials, type OnrampOptions, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type ReflectOptions, type RefractOptions, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, Subscription, type SubscriptionEvent, type ToolInfo, type ToolMeta, SUBPROTOCOL as WS_SUBPROTOCOL, WsTransport, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, isDgUrl, refreshCaCert, registerAndExchange, registerIdentity, registerOnly, rotateIdentity, saveIdentity, version };
|
|
2385
|
+
export { AuthCodeError, type AuthCodeErrorKind, AuthCodeFlow, AuthCodeProvider, type AuthConfig, AuthError, type AuthServerMetadata, type Byok, type ChartOptions, Client, type ClientAuth, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DEFAULT_SCOPE, GRANT_TYPE as DELEGATION_GRANT_TYPE, SERVER_ERROR_CODES as DELEGATION_SERVER_ERROR_CODES, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, DelegatedProvider, type DelegatedToken, DelegationError, type DelegationErrorKind, DelegationRequest, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type Grant, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, LoopbackListener, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, type NamedTokenTypeUrn, NetworkError, NotInitializedError, OAuthTokenProvider, type OnrampCredentials, type OnrampOptions, type PendingAuthorization, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type Redirect, type ReflectOptions, type RefractOptions, type RegisteredClient, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, type ServerErrorCode, Subscription, type SubscriptionEvent, TOKEN_TYPES, TokenSource, type TokenSourceKind, type TokenType, type TokenTypeName, type ToolInfo, type ToolMeta, SUBPROTOCOL as WS_SUBPROTOCOL, WsTransport, authCodeProviderFrom, challengeS256, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, generateVerifier, isDelegatedTokenExpired, isDgUrl, isGrantExpired, isGrantRefreshable, refreshCaCert, refreshGrant, registerAndExchange, registerIdentity, registerOnly, rotateIdentity, saveIdentity, supportsS256, tokenTypeName, version };
|