3 const { mf2
} = require('microformats-parser');
4 const { parse: parseLinkHeader
} = require('@squeep/web-linking');
5 const { Iconv
} = require('iconv');
6 const { version: packageVersion
, name: packageName
} = require('../package.json');
7 const { randomBytes
, createHash
} = require('crypto');
8 const { promisify
} = require('util');
9 const randomBytesAsync
= promisify(randomBytes
);
10 const { Address4
, Address6
} = require('ip-address');
11 const dns
= require('dns');
12 const common
= require('./common');
13 const Enum
= require('./enum');
14 const { ValidationError
} = require('./errors');
16 const _fileScope
= common
.fileScope(__filename
);
18 const noDotPathRE
= /(\/\.\/|\/\.\.\/)/;
19 const v6HostRE
= /\[[0-9a-f:]+\]/;
20 const loopback4
= new Address4('127.0.0.0/8');
21 const scopeSplitRE
= / +/;
25 * @param {Console} logger
26 * @param {Object} options
27 * @param {Number=} options.timeout
28 * @param {Object=} options.userAgent
29 * @param {String=} options.userAgent.product
30 * @param {String=} options.userAgent.version
31 * @param {String=} options.userAgent.implementation
33 constructor(logger
, options
= {}) {
35 this.options
= options
;
37 this._defaultAccept
= options
?.defaultAccept
|| 'text/html, text/*;q=0.9, application/xhtml+xml;q=0.8, application/xml;q=0.8, */*;q=0.1';
38 this._jsonAccept
= options
?.jsonAccept
|| [Enum
.ContentType
.ApplicationJson
, Enum
.ContentType
.Any
+ ';q=0.1'].join(', ');
41 this.got
= this._init
; // Do the dynamic import on first attempt to use client.
46 * Do a little dance to support this ESM client.
48 async
_init(...args
) {
50 // For some reason eslint is confused about import being supported here.
51 // eslint-disable-next-line
52 this.Got
= await
import('got');
53 this.got
= this.Got
.got
.extend({
55 [Enum
.Header
.UserAgent
]: Communication
._userAgentString(this.options
.userAgent
),
56 [Enum
.Header
.Accept
]: this._defaultAccept
,
59 request: this.options
.timeout
|| 120000,
69 return this.got(...args
);
75 * Take notes on transient retries.
77 * @param {*} retryCount
79 _onRetry(error
, retryCount
) {
80 const _scope
= _fileScope('_onRetry');
81 this.logger
.debug(_scope
, 'retry', { retryCount
, error
});
86 * Encode hashed verifier data for PKCE.
87 * @param {BinaryLike} verifier
90 static _challengeFromVerifier(verifier
) {
91 const hash
= createHash('sha256');
92 hash
.update(verifier
);
93 return hash
.digest('base64url');
99 * @property {String} codeChallengeMethod
100 * @property {String} codeVerifier
101 * @property {String} codeChallenge
104 * Create a code verifier and its challenge.
105 * @param {Number} length of verifier string, between 43 and 128
106 * @returns {Promise<PKCEData>}
108 static async
generatePKCE(length
= 128) {
109 if (length
< 43 || length
> 128) {
110 throw new RangeError('InvalidLength');
113 const bufferLength
= Math
.floor(length
* 3 / 4);
114 const randomBuffer
= await
randomBytesAsync(bufferLength
);
115 const verifier
= randomBuffer
.toString('base64url');
117 const challenge
= Communication
._challengeFromVerifier(verifier
);
120 codeChallengeMethod: 'S256',
121 codeVerifier: verifier
,
122 codeChallenge: challenge
,
128 * Check a challenge with a verifier.
129 * @param {String} codeChallenge
130 * @param {String} codeVerifier
131 * @param {String} codeChallengeMethod
134 static verifyChallenge(codeChallenge
, codeVerifier
, codeChallengeMethod
) {
135 switch (codeChallengeMethod
) {
138 const challenge
= Communication
._challengeFromVerifier(codeVerifier
);
139 return challenge
=== codeChallenge
;
143 throw new Error('unsupported challenge method');
149 * Assemble a suitable User-Agent value.
150 * @param {Object} userAgentConfig
151 * @param {String=} userAgentConfig.product
152 * @param {String=} userAgentConfig.version
153 * @param {String=} userAgentConfig.implementation
156 static _userAgentString(userAgentConfig
) {
157 // eslint-disable-next-line security/detect-object-injection
158 const _conf
= (field
, def
) => (userAgentConfig
&& field
in userAgentConfig
) ? userAgentConfig
[field
] : def
;
159 const product
= _conf('product', packageName
).split('/').pop();
160 const version
= _conf('version', packageVersion
);
161 let implementation
= _conf('implementation', Enum
.Specification
);
162 if (implementation
) {
163 implementation
= ` (${implementation})`;
165 return `${product}/${version}${implementation}`;
170 * Isolate the base of a url.
171 * mf2 parser needs this so that relative links can be made absolute.
172 * @param {URL} urlObj
175 static _baseUrlString(urlObj
) {
176 const baseUrl
= new URL(urlObj
);
177 const lastSlashIdx
= baseUrl
.pathname
.lastIndexOf('/');
178 if (lastSlashIdx
> 0) {
179 baseUrl
.pathname
= baseUrl
.pathname
.slice(0, lastSlashIdx
+ 1);
186 * Convert a Content-Type string to normalized components.
188 * N.B. this ill-named non-parsing implementation will not work
189 * if a parameter value for some reason includes a ; or = within
191 * @param {String} contentTypeHeader
192 * @returns {Object} contentType
193 * @returns {String} contentType.mediaType
194 * @returns {Object} contentType.params
196 static _parseContentType(contentTypeHeader
, defaultContentType
= Enum
.ContentType
.ApplicationOctetStream
) {
197 const [ mediaType
, ...params
] = (contentTypeHeader
|| '').split(/ *; */
);
199 mediaType: mediaType
.toLowerCase() || defaultContentType
,
200 params: params
.reduce((obj
, param
) => {
201 const [field
, value
] = param
.split('=');
202 const isQuoted
= value
?.startsWith('"') && value
?.endsWith('"');
203 obj
[field
.toLowerCase()] = isQuoted
? value
.slice(1, value
.length
- 1) : value
;
211 * Parse and add any header link relations from response to microformat data.
212 * @param {Object} microformat
213 * @param {Object} response
214 * @param {Object} response.headers
216 _mergeLinkHeader(microformat
, response
) {
217 const _scope
= _fileScope('_mergeLinkHeader');
219 // Establish that microformat has expected structure
220 ['rels', 'rel-urls'].forEach((p
) => {
221 if (!(p
in microformat
)) {
222 microformat
[p
] = {}; // eslint-disable-line security/detect-object-injection
225 if (!('items' in microformat
)) {
226 microformat
.items
= [];
229 const linkHeader
= response
.headers
[Enum
.Header
.Link
.toLowerCase()];
233 links
.push(...parseLinkHeader(linkHeader
));
235 this.logger
.error(_scope
, 'failed to parse link header', { error: e
, linkHeader
});
240 // Push header link rels into microformat form.
241 // Inserted at front of lists, as headers take precedence.
242 links
.forEach((link
) => {
243 link
.attributes
.forEach((attr
) => {
244 if (attr
.name
=== 'rel') {
245 if (!(attr
.value
in microformat
.rels
)) {
246 microformat
.rels
[attr
.value
] = [];
248 microformat
.rels
[attr
.value
].unshift(link
.target
);
250 if (!(link
.target
in microformat
['rel-urls'])) {
251 microformat
['rel-urls'][link
.target
] = {
256 microformat
['rel-urls'][link
.target
].rels
.unshift(attr
.value
);
264 * Retrieve and parse microformat data from url.
265 * N.B. this absorbs any errors!
266 * @param {URL} urlObj
267 * @returns {Promise<Object>}
269 async
fetchMicroformat(urlObj
) {
270 const _scope
= _fileScope('fetchMicroformat');
271 const logInfoData
= {
273 microformat: undefined,
278 const fetchMicroformatConfig
= {
281 responseType: 'buffer',
283 response
= await
this.got(fetchMicroformatConfig
);
285 this.logger
.error(_scope
, 'microformat request failed', { error: e
, ...logInfoData
});
288 logInfoData
.response
= common
.gotResponseLogData(response
);
290 // Normalize to utf8.
292 const contentType
= Communication
._parseContentType(response
.headers
[Enum
.Header
.ContentType
.toLowerCase()]);
293 // If a charset was specified, and it's not utf8ish, attempt to transliterate it to utf8.
294 const nonUTF8Charset
= !/utf-*8/i.test(contentType
.params
.charset
) && contentType
.params
.charset
;
295 if (nonUTF8Charset
) {
297 const iconv
= new Iconv(nonUTF8Charset
, 'utf-8//translit//ignore');
298 body
= iconv
.convert(response
.body
).toString('utf8');
300 // istanbul ignore next
301 this.logger
.error(_scope
, 'iconv conversion error', { error: e
, ...logInfoData
});
302 // Try to carry on, maybe the encoding will work anyhow...
305 body
= response
.body
.toString('utf8');
308 let microformat
= {};
310 microformat
= mf2(body
, {
311 baseUrl: Communication
._baseUrlString(urlObj
),
314 this.logger
.error(_scope
, 'failed to parse microformat data', { error: e
, ...logInfoData
});
315 // Try to carry on, maybe there are link headers...
318 this._mergeLinkHeader(microformat
, response
);
320 logInfoData
.microformat
= microformat
;
322 this.logger
.debug(_scope
, 'parsed microformat data', logInfoData
);
328 * Retrieve and parse JSON.
329 * N.B. this absorbs any errors!
330 * @param {URL} urlObj
331 * @returns {Promise<Object>}
333 async
fetchJSON(urlObj
) {
334 const _scope
= _fileScope('fetchJSON');
335 const logInfoData
= {
341 const fetchJSONConfig
= {
345 [Enum
.Header
.Accept
]: this._jsonAccept
,
347 responseType: 'json',
349 response
= await
this.got(fetchJSONConfig
);
351 this.logger
.error(_scope
, 'json request failed', { error: e
, ...logInfoData
});
354 logInfoData
.response
= common
.gotResponseLogData(response
);
356 return response
.body
;
361 * Validate a url has a specific schema.
362 * @param {URL} urlObj
363 * @param {String[]} validSchemes
365 static _urlValidScheme(urlObj
, validSchemes
= ['http:', 'https:']) {
366 if (!validSchemes
.includes(urlObj
.protocol
)) {
367 throw new ValidationError(`unsupported url scheme '${urlObj.protocol}'`);
373 * Validate a url does not include some components.
374 * @param {URL} urlObj
375 * @param {String[]} disallowed
377 static _urlPartsDisallowed(urlObj
, disallowed
) {
378 disallowed
.forEach((part
) => {
379 if (urlObj
[part
]) { // eslint-disable-line security/detect-object-injection
380 throw new ValidationError(`url cannot contain ${common.properURLComponentName(part)}`);
387 * Validate a url does not have relative path.
388 * @param {String} url
390 static _urlPathNoDots(url
) {
391 if (noDotPathRE
.test(url
)) {
392 throw new ValidationError('relative path segment not valid');
398 * Validate a url does not have a hostname which is an ip address.
399 * N.B. Sets isLoopback on urlObj
400 * @param {URL} urlObj
401 * @param {Boolean} allowLoopback
402 * @returns {Promise<void>}
404 static async
_urlNamedHost(urlObj
, allowLoopback
, resolveHostname
) {
406 if (v6HostRE
.test(urlObj
.hostname
)) {
408 * We do not need to worry about the Address6() failing to parse,
409 * as if it looks like an ipv6 addr but is not valid, the URL()
410 * call would already have failed.
412 address
= new Address6(urlObj
.hostname
.slice(1, urlObj
.hostname
.length
- 1));
413 /* succeeded parsing as ipv6, reject unless loopback */
414 urlObj
.isLoopback
= address
.isLoopback();
417 address
= new Address4(urlObj
.hostname
);
418 /* succeeded parsing as ipv4, reject unless loopback */
419 urlObj
.isLoopback
= address
.isInSubnet(loopback4
);
421 /* did not parse as ip, carry on */
425 if (resolveHostname
&& !urlObj
.isLoopback
) {
427 * Resolve hostname to check for localhost.
428 * This is more complicated due to SSRF mitigation:
429 * If the hostname does not end with a ., we also resolve that,
430 * and complain if the two resolutions do not match, assuming
431 * malicious intent for the server to resolve a local record.
433 const hostnames
= [urlObj
.hostname
];
434 if (!urlObj
.hostname
.endsWith('.')) {
435 hostnames
.push(urlObj
.hostname
+ '.');
437 const settledResolutions
= await Promise
.allSettled(hostnames
.map((hostname
) => dns
.promises
.lookup(hostname
, {
441 // If any resolution failed, bail.
442 if (settledResolutions
443 .map((resolution
) => resolution
.status
)
444 .includes('rejected')) {
445 throw new ValidationError('could not resolve hostname');
448 // extract each resolution value, array of {address,family}
449 const resolutions
= settledResolutions
.map((resolution
) => resolution
.value
);
451 // If there were two resolutions, ensure they returned identical results.
452 if (resolutions
.length
> 1) {
453 // create set of addresses for each resolution
454 const addressSets
= resolutions
.map((addrs
) => {
455 return new Set((addrs
|| []).map((a
) => a
.address
));
457 const differences
= common
.setSymmetricDifference(...addressSets
);
458 if (differences
.size
) {
459 throw new ValidationError('inconsistent hostname resolution');
462 const resolvedHost
= resolutions
[0] || [];
464 // Persist the loopback state
465 urlObj
.isLoopback
= resolvedHost
.reduce((acc
, resolved
) => {
467 switch (resolved
.family
) {
469 addr
= new Address4(resolved
.address
);
470 return acc
|| addr
.isInSubnet(loopback4
);
472 addr
= new Address6(resolved
.address
);
473 return acc
|| addr
.isLoopback();
481 && (!urlObj
.isLoopback
|| !allowLoopback
)) {
482 throw new ValidationError('hostname cannot be IP');
488 * Ensure a url meets the requirements to be a profile uri.
489 * @param {String} url
490 * @param {Object} validationOptions
491 * @param {Boolean} validationOptions.allowLoopback
492 * @param {Boolean} validationOptions.resolveHostname
493 * @returns {Promise<void>}
495 async
validateProfile(url
, validationOptions
) {
496 const _scope
= _fileScope('validateProfile');
497 const errorScope
= 'invalid profile url';
499 const options
= Object
.assign({
500 allowLoopback: false,
501 resolveHostname: false,
502 }, validationOptions
);
506 profile
= new URL(url
);
508 this.logger
.debug(_scope
, 'failed to parse url', { error: e
, url
});
509 throw new ValidationError(`${errorScope}: unparsable`);
511 profile
.isLoopback
= false;
514 Communication
._urlValidScheme(profile
);
515 Communication
._urlPartsDisallowed(profile
, ['hash', 'username', 'password', 'port']);
516 Communication
._urlPathNoDots(url
);
517 await Communication
._urlNamedHost(profile
, options
.allowLoopback
, options
.resolveHostname
);
519 this.logger
.debug(_scope
, 'profile url not valid', { url
, error: e
});
520 throw new ValidationError(`${errorScope}: ${e.message}`);
528 * Ensure a url meets the requirements to be a client identifier.
529 * Sets 'isLoopback' on returned URL object to true if hostname is or resolves to a loopback ip.
530 * @param {String} url
531 * @param {Object} validationOptions
532 * @param {Boolean} validationOptions.allowLoopback
533 * @param {Boolean} validationOptions.resolveHostname
534 * @returns {Promise<URL>}
536 async
validateClientIdentifier(url
, validationOptions
) {
537 const _scope
= _fileScope('validateClientIdentifier');
538 const errorScope
= 'invalid client identifier url';
540 const options
= Object
.assign({
542 resolveHostname: true,
543 }, validationOptions
);
547 clientId
= new URL(url
);
549 this.logger
.debug(_scope
, 'failed to parse url', { error: e
, url
});
550 throw new ValidationError('invalid client identifier url: unparsable');
552 clientId
.isLoopback
= false;
555 Communication
._urlValidScheme(clientId
);
556 Communication
._urlPartsDisallowed(clientId
, ['hash', 'username', 'password']);
557 Communication
._urlPathNoDots(url
);
558 await Communication
._urlNamedHost(clientId
, options
.allowLoopback
, options
.resolveHostname
);
560 this.logger
.debug(_scope
, 'client identifier url not valid', { url
, error: e
});
561 throw new ValidationError(`${errorScope}: ${e.message}`);
569 * @typedef {Object} ClientIdentifierData
570 * @property {Object} rels - keyed by relation to array of uris
571 * @property {HAppData[]} items
574 * Retrieve and parse client identifier endpoint data.
575 * N.B. Assumes urlObj has passed validateClientIdentifier.
576 * @param {URL} urlObj
577 * @returns {ClientIdentifierData|undefined} mf2 data filtered for h-app items, or undefined if url could not be fetched
579 async
fetchClientIdentifier(urlObj
) {
580 const _scope
= _fileScope('fetchClientIdentifier');
582 // Loopback address will eschew client fetch, return empty data.
583 const isLoopbackResult
= {
588 // Set by validation method in case of loopback ip hostname
589 if (urlObj
.isLoopback
) {
590 return isLoopbackResult
;
593 const mfData
= await
this.fetchMicroformat(urlObj
);
598 // Only return h-app items with matching url field.
600 rels: mfData
.rels
|| {},
601 items: (mfData
.items
|| []).filter((item
) => {
602 let urlMatched
= false;
603 const itemType
= item
.type
|| [];
604 if ((itemType
.includes('h-app') || itemType
.includes('h-x-app'))
605 && (item
?.properties
?.url
)) {
606 item
.properties
.url
.forEach((url
) => {
608 const hUrl
= new URL(url
);
609 if (hUrl
.href
=== urlObj
.href
) {
622 * @typedef ProfileData
623 * @property {String} name
624 * @property {String} photo
625 * @property {String} url
626 * @property {String} email
627 * @property {String} authorizationEndpoint - deprecated, backwards compatibility for 20201126 spec
628 * @property {String} tokenEndpoint - deprecated, backwards compatibility for 20201126 spec
629 * @property {String} indieauthMetadata authorization server metadata endpoint
630 * @property {Object} metadata - authorization server metadata for profile
631 * @property {String} metadata.issuer
632 * @property {String} metadata.authorizationEndpoint
633 * @property {String} metadata.tokenEndpoint
634 * @property {String} metadata.ticketEndpoint
635 * @property {String} metadata.introspectionEndpoint
636 * @property {String} metadata.introspectionEndpointAuthMethodsSupported
637 * @property {String} metadata.revocationEndpoint
638 * @property {String} metadata.revocationEndpointAuthMethodsSupported
639 * @property {String} metadata.scopesSupported
640 * @property {String} metadata.responseTypesSupported
641 * @property {String} metadata.grantTypesSupported
642 * @property {String} metadata.serviceDocumentation
643 * @property {String} metadata.codeChallengeMethodsSupported
644 * @property {String} metadata.authorizationResponseIssParameterSupported
645 * @property {String} metadata.userinfoEndpoint
648 * Fetch the relevant microformat data from profile url h-card information,
649 * and authorization server metadata.
650 * @param {URL} urlObj
651 * @returns {ProfileData} mf2 data filtered for select fields from h-card
653 async
fetchProfile(urlObj
) {
654 const _scope
= _fileScope('fetchProfile');
656 const mfData
= await
this.fetchMicroformat(urlObj
);
665 // Locate h-card mf2 items with url field matching profile url,
666 // and populate profile fields with first-encountered card values.
667 if (mfData
&& 'items' in mfData
) {
668 const hCards
= mfData
.items
.filter((item
) =>
669 item
?.type
?.includes('h-card') &&
670 item
.properties
&& item
.properties
.url
&& item
.properties
.url
.includes(urlObj
.href
));
671 hCards
.forEach((hCard
) => {
672 Object
.keys(profile
).forEach((key
) => {
673 if (!profile
[key
] && key
in hCard
.properties
) { // eslint-disable-line security/detect-object-injection
674 profile
[key
] = hCard
.properties
[key
][0]; // eslint-disable-line security/detect-object-injection
680 // Populate legacy mf2 fields from relation links.
681 // These will be overwritten if they also exist in server metadata.
683 authorizationEndpoint: 'authorization_endpoint', // backwards compatibility
684 tokenEndpoint: 'token_endpoint', // backwards compatibility
685 ticketEndpoint: 'ticket_endpoint', // backwards compatibility
686 }).forEach(([p
, r
]) => {
687 if (mfData
&& r
in mfData
.rels
) {
688 profile
.metadata
[p
] = profile
[p
] = mfData
.rels
[r
][0]; // eslint-disable-line security/detect-object-injection
692 // Set metadata field.
693 if (mfData
&& 'indieauth-metadata' in mfData
.rels
) {
694 profile
.indieauthMetadata
= mfData
.rels
['indieauth-metadata'][0];
697 // Attempt to populate metadata from authorization server.
698 if (profile
.indieauthMetadata
) {
701 mdURL
= new URL(profile
.indieauthMetadata
);
702 } catch (e
) /* istanbul ignore next */ {
703 this.logger
.error(_scope
, 'invalid authorization server metadata url', { profile
});
705 /* istanbul ignore else */
707 const metadataResponse
= await
this.fetchJSON(mdURL
);
708 if (metadataResponse
) {
709 // Map snake_case fields to camelCase.
712 authorizationEndpoint: 'authorization_endpoint',
713 tokenEndpoint: 'token_endpoint',
714 ticketEndpoint: 'ticket_endpoint',
715 introspectionEndpoint: 'introspection_endpoint',
716 introspectionEndpointAuthMethodsSupported: 'introspection_endpoint_auth_methods_supported',
717 revocationEndpoint: 'revocation_endpoint',
718 revocationEndpointAuthMethodsSupported: 'revocation_endpoint_auth_methods_supported',
719 scopesSupported: 'scopes_supported',
720 responseTypesSupported: 'response_types_supported',
721 grantTypesSupported: 'grant_types_supported',
722 serviceDocumentation: 'service_documentation',
723 codeChallengeMethodsSupported: 'code_challenge_methods_supported',
724 authorizationResponseIssParameterSupported: 'authorization_response_iss_parameter_supported',
725 userinfoEndpoint: 'userinfo_endpoint',
726 }).forEach(([c
, s
]) => {
727 if (s
in metadataResponse
) {
728 profile
.metadata
[c
] = metadataResponse
[s
]; // eslint-disable-line security/detect-object-injection
732 // Populate legacy profile fields.
733 ['authorizationEndpoint', 'tokenEndpoint', 'ticketEndpoint'].forEach((f
) => {
734 if (f
in profile
.metadata
) {
735 profile
[f
] = profile
.metadata
[f
]; // eslint-disable-line security/detect-object-injection
747 * POST to the auth endpoint, to redeem a code for a profile or token.
748 * N.B. this absorbs any errors!
749 * @param {URL} urlObj
750 * @param {String} code
751 * @param {String} codeVerifier
752 * @param {String} clientId
753 * @param {String} redirectURI
756 async
redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
) {
757 const _scope
= _fileScope('redeemCode');
759 const postRedeemCodeConfig
= {
763 [Enum
.Header
.Accept
]: this._jsonAccept
,
766 'grant_type': 'authorization_code',
768 'client_id': clientId
,
769 'redirect_uri': redirectURI
,
770 'code_verifier': codeVerifier
,
772 responseType: 'json',
776 const response
= await
this.got(postRedeemCodeConfig
);
777 return response
.body
;
779 this.logger
.error(_scope
, 'redeem code request failed', { error: e
, url: urlObj
.href
});
786 * Deprecated method name alias.
788 * @param {URL} urlObj
789 * @param {String} code
790 * @param {Strin} codeVerifier
791 * @param {String} clientId
792 * @param {String} redirectURI
795 async
redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
) {
796 return this.redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
801 * Verify a token with an IdP endpoint, using the Authorization header supplied.
802 * @param {URL} introspectionUrlObj
803 * @param {String} authorizationHeader
804 * @param {String} token
806 async
introspectToken(introspectionUrlObj
, authorizationHeader
, token
) {
807 const _scope
= _fileScope('introspectToken');
809 const postIntrospectConfig
= {
810 url: introspectionUrlObj
,
813 [Enum
.Header
.Authorization
]: authorizationHeader
,
814 [Enum
.Header
.Accept
]: this._jsonAccept
,
819 responseType: 'json',
823 const response
= await
this.got(postIntrospectConfig
);
824 this.logger
.debug(_scope
, 'response', { response
});
836 if (![true, false].includes(active
)) {
837 throw new RangeError('missing required response field "active"');
843 ...(clientId
&& { clientId
}),
844 ...(scope
&& { scope: scope
.split(scopeSplitRE
) }),
845 ...(exp
&& { exp: Number(exp
) }),
846 ...(iat
&& { iat: Number(iat
) }),
849 this.logger
.error(_scope
, 'failed to parse json', { error: e
, response: common
.gotResponseLogData(response
) });
853 this.logger
.error(_scope
, 'introspect token request failed', { error: e
, url: introspectionUrlObj
.href
});
860 * Attempt to deliver a ticket to an endpoint.
861 * N.B. does not absorb errors
862 * @param {URL} ticketEndpointUrlObj
863 * @param {URL} resourceUrlObj
864 * @param {URL} subjectUrlObj
865 * @param {String} ticket
866 * @returns {Promise<AxiosResponse>}
868 async
deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
) {
869 const _scope
= _fileScope('deliverTicket');
872 const ticketConfig
= {
874 url: ticketEndpointUrlObj
,
877 resource: resourceUrlObj
.href
,
878 subject: subjectUrlObj
.href
,
881 return await
this.got(ticketConfig
);
883 this.logger
.error(_scope
, 'ticket delivery request failed', { error: e
, url: ticketEndpointUrlObj
.href
});
890 module
.exports
= Communication
;