+
+ /**
+ * Attempt to fetch some link relations from a url.
+ * @param {URL} urlObj url
+ * @returns {Promise<object>} data
+ */
+ async _fetchMetadataOrTokenEndpoint(urlObj) {
+ const _scope = _fileScope('_fetchMetadataOrTokenEndpoint');
+
+ let metadataUrl, tokenUrl;
+ if (urlObj) {
+ const mfData = await this.fetchMicroformat(urlObj);
+ const metadataRel = mfData?.rels?.[MF2Rel.IndieauthMetadata]?.[0];
+ if (metadataRel) {
+ try {
+ metadataUrl = new URL(metadataRel);
+ } catch (e) { // eslint-disable-line no-unused-vars
+ this.logger.debug(_scope, 'invalid metadata rel url', { url: urlObj.href, metadataRel });
+ }
+ }
+ if (!metadataUrl) {
+ // No metadata rel, try old-style token endpoint
+ const tokenRel = mfData?.rels?.[MF2Rel.TokenEndpoint]?.[0];
+ if (tokenRel) {
+ try {
+ tokenUrl = new URL(tokenRel);
+ } catch (e) { // eslint-disable-line no-unused-vars
+ this.logger.debug(_scope, 'invalid token rel url', { url: urlObj.href, tokenRel });
+ }
+ }
+ }
+ }
+ return { metadataUrl, tokenUrl };
+ }
+
+
+ /**
+ * Attempt to redeem a ticket for a token.
+ * N.B. does not absorb errors
+ * @param {string} ticket ticket
+ * @param {URL} resourceUrlObj resource url
+ * @param {URL=} issuerUrlObj issuer url
+ * @returns {Promise<object>} response body
+ */
+ async redeemTicket(ticket, resourceUrlObj, issuerUrlObj) {
+ const _scope = _fileScope('redeemTicket');
+
+ let metadataUrl, tokenUrl;
+ // Attempt to determine metadata or token endpoint from issuer MF data
+ if (issuerUrlObj) {
+ ({ metadataUrl, tokenUrl } = await this._fetchMetadataOrTokenEndpoint(issuerUrlObj));
+ }
+
+ // Fallback to resource MF data
+ if (!metadataUrl && !tokenUrl) {
+ ({ metadataUrl, tokenUrl } = await this._fetchMetadataOrTokenEndpoint(resourceUrlObj));
+ }
+
+ if (metadataUrl) {
+ const metadata = await this.fetchMetadata(metadataUrl);
+ try {
+ tokenUrl = new URL(metadata?.tokenEndpoint);
+ } catch (e) { // eslint-disable-line no-unused-vars
+ this.logger.debug(_scope, 'invalid token endpoint url from metadata', { resourceUrl: resourceUrlObj.href, issuerUrl: issuerUrlObj.href, tokenEndpoint: metadata?.tokenEndpoint });
+ }
+ }
+
+ if (!tokenUrl) {
+ throw new ValidationError('could not determine endpoint for ticket redemption');
+ }
+
+ const postRedeemTicketConfig = {
+ url: tokenUrl,
+ method: 'POST',
+ headers: {
+ [Enum.Header.Accept]: this._jsonAccept,
+ },
+ form: {
+ 'grant_type': 'ticket',
+ ticket,
+ },
+ responseType: 'json',
+ };
+
+ try {
+ const response = await this.got(postRedeemTicketConfig);
+ return response.body;
+ } catch (e) {
+ this.logger.error(_scope, 'ticket redemption failed', { error: e, resource: resourceUrlObj.href, issuer: issuerUrlObj?.href });
+ throw e;
+ }
+ }