UNPKG

@esri/arcgis-rest-request

Version:

Common methods and utilities for @esri/arcgis-rest-js packages.

1,065 lines (1,043 loc) 147 kB
/* @preserve * @esri/arcgis-rest-request - v4.5.0 - Apache-2.0 * Copyright (c) 2017-2025 Esri, Inc. * Tue May 20 2025 22:42:33 GMT+0000 (Coordinated Universal Time) */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.arcgisRest = global.arcgisRest || {})); })(this, (function (exports) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Checks parameters to see if we should use FormData to send the request * @param params The object whose keys will be encoded. * @return A boolean indicating if FormData will be required. */ function requiresFormData(params) { return Object.keys(params).some((key) => { let value = params[key]; if (!value) { return false; } if (value && value.toParam) { value = value.toParam(); } const type = value.constructor.name; switch (type) { case "Array": return false; case "Object": return false; case "Date": return false; case "Function": return false; case "Boolean": return false; case "String": return false; case "Number": return false; default: return true; } }); } /** * Converts parameters to the proper representation to send to the ArcGIS REST API. * @param params The object whose keys will be encoded. * @return A new object with properly encoded values. */ function processParams(params) { const newParams = {}; Object.keys(params).forEach((key) => { var _a, _b; let param = params[key]; if (param && param.toParam) { param = param.toParam(); } if (!param && param !== 0 && typeof param !== "boolean" && typeof param !== "string") { return; } const type = param.constructor.name; let value; // properly encodes objects, arrays and dates for arcgis.com and other services. // ported from https://github.com/Esri/esri-leaflet/blob/master/src/Request.js#L22-L30 // also see https://github.com/Esri/arcgis-rest-js/issues/18: // null, undefined, function are excluded. If you want to send an empty key you need to send an empty string "". switch (type) { case "Array": // Based on the first element of the array, classify array as an array of arrays, an array of objects // to be stringified, or an array of non-objects to be comma-separated // eslint-disable-next-line no-case-declarations const firstElementType = (_b = (_a = param[0]) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name; value = firstElementType === "Array" ? param // pass thru array of arrays : firstElementType === "Object" ? JSON.stringify(param) // stringify array of objects : param.join(","); // join other types of array elements break; case "Object": value = JSON.stringify(param); break; case "Date": value = param.valueOf(); break; case "Function": value = null; break; case "Boolean": value = param + ""; break; default: value = param; break; } if (value || value === 0 || typeof value === "string" || Array.isArray(value)) { newParams[key] = value; } }); return newParams; } /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Encodes keys and parameters for use in a URL's query string. * * @param key Parameter's key * @param value Parameter's value * @returns Query string with key and value pairs separated by "&" */ function encodeParam(key, value) { // For array of arrays, repeat key=value for each element of containing array if (Array.isArray(value) && value[0] && Array.isArray(value[0])) { return value .map((arrayElem) => encodeParam(key, arrayElem)) .join("&"); } return encodeURIComponent(key) + "=" + encodeURIComponent(value); } /** * Encodes the passed object as a query string. * * @param params An object to be encoded. * @returns An encoded query string. */ function encodeQueryString(params) { const newParams = processParams(params); return Object.keys(newParams) .map((key) => { return encodeParam(key, newParams[key]); }) .join("&"); } const FormData = globalThis.FormData; const File = globalThis.File; const Blob$1 = globalThis.Blob; /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Encodes parameters in a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object in browsers or in a [FormData](https://github.com/form-data/form-data) in Node.js * * @param params An object to be encoded. * @returns The complete [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. */ function encodeFormData(params, forceFormData) { // see https://github.com/Esri/arcgis-rest-js/issues/499 for more info. const useFormData = requiresFormData(params) || forceFormData; const newParams = processParams(params); if (useFormData) { const formData = new FormData(); Object.keys(newParams).forEach((key) => { if (typeof Blob !== "undefined" && newParams[key] instanceof Blob) { /* To name the Blob: 1. look to an alternate request parameter called 'fileName' 2. see if 'name' has been tacked onto the Blob manually 3. if all else fails, use the request parameter */ const filename = newParams["fileName"] || newParams[key].name || key; formData.append(key, newParams[key], filename); } else { formData.append(key, newParams[key]); } }); return formData; } else { return encodeQueryString(params); } } /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * This represents a generic error from an ArcGIS endpoint. There will be details about the error in the {@linkcode ArcGISRequestError.message}, {@linkcode ArcGISRequestError.originalMessage} properties on the error. You * can also access the original server response at {@linkcode ArcGISRequestError.response} which may have additional details. * * ```js * request(someUrl, someOptions).catch(e => { * if(e.name === "ArcGISRequestError") { * console.log("Something went wrong with the request:", e); * console.log("Full server response", e.response); * } * }) * ``` */ class ArcGISRequestError extends Error { /** * Create a new `ArcGISRequestError` object. * * @param message - The error message from the API * @param code - The error code from the API * @param response - The original response from the API that caused the error * @param url - The original url of the request * @param options - The original options and parameters of the request */ constructor(message, code, response, url, options) { // 'Error' breaks prototype chain here super(message); // restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class // we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now. // Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work // and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); message = message || "UNKNOWN_ERROR"; code = code || "UNKNOWN_ERROR_CODE"; this.name = "ArcGISRequestError"; this.message = code === "UNKNOWN_ERROR_CODE" ? message : `${code}: ${message}`; this.originalMessage = message; this.code = code; this.response = response; this.url = url; this.options = options; } } /* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Method used internally to surface messages to developers. */ function warn(message) { if (console && console.warn) { console.warn.apply(console, [message]); } } function getFetch() { return Promise.resolve({ fetch: globalThis.fetch, Headers: globalThis.Headers, Response: globalThis.Response, Request: globalThis.Request }); } /* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ const NODEJS_DEFAULT_REFERER_HEADER = `@esri/arcgis-rest-js`; /** * Sets the default options that will be passed in **all requests across all `@esri/arcgis-rest-js` modules**. * * ```js * import { setDefaultRequestOptions } from "@esri/arcgis-rest-request"; * * setDefaultRequestOptions({ * authentication: ArcGISIdentityManager // all requests will use this session by default * }) * ``` * * You should **never** set a default `authentication` when you are in a server side environment where you may be handling requests for many different authenticated users. * * @param options The default options to pass with every request. Existing default will be overwritten. * @param hideWarnings Silence warnings about setting default `authentication` in shared environments. */ function setDefaultRequestOptions(options, hideWarnings) { if (options.authentication && !hideWarnings) { warn("You should not set `authentication` as a default in a shared environment such as a web server which will process multiple users requests. You can call `setDefaultRequestOptions` with `true` as a second argument to disable this warning."); } globalThis.DEFAULT_ARCGIS_REQUEST_OPTIONS = options; } function getDefaultRequestOptions() { return (globalThis.DEFAULT_ARCGIS_REQUEST_OPTIONS || { httpMethod: "POST", params: { f: "json" } }); } /** * This error is thrown when a request encounters an invalid token error. Requests that use {@linkcode ArcGISIdentityManager} or * {@linkcode ApplicationCredentialsManager} in the `authentication` option the authentication manager will automatically try to generate * a fresh token using either {@linkcode ArcGISIdentityManager.refreshCredentials} or * {@linkcode ApplicationCredentialsManager.refreshCredentials}. If the request with the new token fails you will receive an `ArcGISAuthError` * if refreshing the token fails you will receive an instance of {@linkcode ArcGISTokenRequestError}. * * ```js * request(someUrl, { * authentication: identityManager, * // some additional options... * }).catch(e => { * if(e.name === "ArcGISAuthError") { * console.log("Request with a new token failed you might want to have the user authorize again.") * } * * if(e.name === "ArcGISTokenRequestError") { * console.log("There was an error refreshing the token you might want to have the user authorize again.") * } * }) * ``` */ class ArcGISAuthError extends ArcGISRequestError { /** * Create a new `ArcGISAuthError` object. * * @param message - The error message from the API * @param code - The error code from the API * @param response - The original response from the API that caused the error * @param url - The original url of the request * @param options - The original options of the request */ constructor(message = "AUTHENTICATION_ERROR", code = "AUTHENTICATION_ERROR_CODE", response, url, options) { super(message, code, response, url, options); this.name = "ArcGISAuthError"; this.message = code === "AUTHENTICATION_ERROR_CODE" ? message : `${code}: ${message}`; // restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class // we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now. // Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work // and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); } retry(getSession, retryLimit = 1) { let tries = 0; const retryRequest = (resolve, reject) => { tries = tries + 1; getSession(this.url, this.options) .then((session) => { const newOptions = Object.assign(Object.assign({}, this.options), { authentication: session }); return internalRequest(this.url, newOptions); }) .then((response) => { resolve(response); }) .catch((e) => { if (e.name === "ArcGISAuthError" && tries < retryLimit) { retryRequest(resolve, reject); } else if (e.name === this.name && e.message === this.message && tries >= retryLimit) { reject(this); } else { reject(e); } }); }; return new Promise((resolve, reject) => { retryRequest(resolve, reject); }); } } /** * Checks for errors in a JSON response from the ArcGIS REST API. If there are no errors, it will return the `data` passed in. If there is an error, it will throw an `ArcGISRequestError` or `ArcGISAuthError`. * * @param data The response JSON to check for errors. * @param url The url of the original request * @param params The parameters of the original request * @param options The options of the original request * @returns The data that was passed in the `data` parameter */ function checkForErrors(response, url, params, options, originalAuthError) { // this is an error message from billing.arcgis.com backend if (response.code >= 400) { const { message, code } = response; throw new ArcGISRequestError(message, code, response, url, options); } // error from ArcGIS Online or an ArcGIS Portal or server instance. if (response.error) { const { message, code, messageCode } = response.error; const errorCode = messageCode || code || "UNKNOWN_ERROR_CODE"; if (code === 498 || code === 499) { if (originalAuthError) { throw originalAuthError; } else { throw new ArcGISAuthError(message, errorCode, response, url, options); } } throw new ArcGISRequestError(message, errorCode, response, url, options); } // error from a status check if (response.status === "failed" || response.status === "failure") { let message; let code = "UNKNOWN_ERROR_CODE"; try { message = JSON.parse(response.statusMessage).message; code = JSON.parse(response.statusMessage).code; } catch (e) { message = response.statusMessage || response.message; } throw new ArcGISRequestError(message, code, response, url, options); } return response; } /** * This is the internal implementation of `request` without the automatic retry behavior to prevent * infinite loops when a server continues to return invalid token errors. * * @param url - The URL of the ArcGIS REST API endpoint. * @param requestOptions - Options for the request, including parameters relevant to the endpoint. * @returns A Promise that will resolve with the data from the response. * @internal */ function internalRequest(url, requestOptions) { const defaults = getDefaultRequestOptions(); const options = Object.assign(Object.assign(Object.assign({ httpMethod: "POST" }, defaults), requestOptions), { params: Object.assign(Object.assign({}, defaults.params), requestOptions.params), headers: Object.assign(Object.assign({}, defaults.headers), requestOptions.headers) }); const { httpMethod, rawResponse } = options; const params = Object.assign({ f: "json" }, options.params); let originalAuthError = null; const fetchOptions = { method: httpMethod, signal: options.signal, /* ensures behavior mimics XMLHttpRequest. needed to support sending IWA cookies */ credentials: options.credentials || "same-origin" }; // the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header // and that request needs to send cookies cross domain // so we need to set the credentials to "include" if (options.headers && options.headers["X-Esri-Auth-Client-Id"] && url.indexOf("/oauth2/platformSelf") > -1) { fetchOptions.credentials = "include"; } let authentication; // Check to see if this is a raw token as a string and create a IAuthenticationManager like object for it. // Otherwise this just assumes that options.authentication is an IAuthenticationManager. if (typeof options.authentication === "string") { const rawToken = options.authentication; authentication = { portal: "https://www.arcgis.com/sharing/rest", getToken: () => { return Promise.resolve(rawToken); } }; /* istanbul ignore else - we don't need to test NOT warning people */ if (!options.authentication.startsWith("AAPK") && !options.authentication.startsWith("AATK") && // doesn't look like an API Key !options.suppressWarnings && // user doesn't want to suppress warnings for this request !globalThis.ARCGIS_REST_JS_SUPPRESS_TOKEN_WARNING // we haven't shown the user this warning yet ) { warn(`Using an oAuth 2.0 access token directly in the token option is discouraged. Consider using ArcGISIdentityManager or Application session. See https://esriurl.com/arcgis-rest-js-direct-token-warning for more information.`); globalThis.ARCGIS_REST_JS_SUPPRESS_TOKEN_WARNING = true; } } else { authentication = options.authentication; } // for errors in GET requests we want the URL passed to the error to be the URL before // query params are applied. const originalUrl = url; return (authentication ? authentication.getToken(url).catch((err) => { /** * append original request url and requestOptions * to the error thrown by getToken() * to assist with retrying */ err.url = url; err.options = options; /** * if an attempt is made to talk to an unfederated server * first try the request anonymously. if a 'token required' * error is thrown, throw the UNFEDERATED error then. */ originalAuthError = err; return Promise.resolve(""); }) : Promise.resolve("")) .then((token) => { if (token.length) { params.token = token; } if (authentication && authentication.getDomainCredentials) { fetchOptions.credentials = authentication.getDomainCredentials(url); } // Custom headers to add to request. IRequestOptions.headers with merge over requestHeaders. const requestHeaders = {}; if (fetchOptions.method === "GET") { // Prevents token from being passed in query params when hideToken option is used. /* istanbul ignore if - window is always defined in a browser. Test case is covered by Jasmine in node test */ if (params.token && options.hideToken && // Sharing API does not support preflight check required by modern browsers https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request typeof window === "undefined") { requestHeaders["X-Esri-Authorization"] = `Bearer ${params.token}`; delete params.token; } // encode the parameters into the query string const queryParams = encodeQueryString(params); // dont append a '?' unless parameters are actually present const urlWithQueryString = queryParams === "" ? url : url + "?" + encodeQueryString(params); if ( // This would exceed the maximum length for URLs by 2000 as default or as specified by the consumer and requires POST (options.maxUrlLength && urlWithQueryString.length > options.maxUrlLength) || (!options.maxUrlLength && urlWithQueryString.length > 2000) || // Or if the customer requires the token to be hidden and it has not already been hidden in the header (for browsers) (params.token && options.hideToken)) { // the consumer specified a maximum length for URLs // and this would exceed it, so use post instead fetchOptions.method = "POST"; // If the token was already added as a Auth header, add the token back to body with other params instead of header if (token.length && options.hideToken) { params.token = token; // Remove existing header that was added before url query length was checked delete requestHeaders["X-Esri-Authorization"]; } } else { // just use GET url = urlWithQueryString; } } /* updateResources currently requires FormData even when the input parameters dont warrant it. https://developers.arcgis.com/rest/users-groups-and-items/update-resources.htm see https://github.com/Esri/arcgis-rest-js/pull/500 for more info. */ const forceFormData = new RegExp("/items/.+/updateResources").test(url); if (fetchOptions.method === "POST") { fetchOptions.body = encodeFormData(params, forceFormData); } // Mixin headers from request options fetchOptions.headers = Object.assign(Object.assign({}, requestHeaders), options.headers); // This should have the same conditional for Node JS as ArcGISIdentityManager.refreshWithUsernameAndPassword() // to ensure that generated tokens have the same referer when used in Node with a username and password. /* istanbul ignore next - karma reports coverage on browser tests only */ if ((typeof window === "undefined" || (window && typeof window.document === "undefined")) && !fetchOptions.headers.referer) { fetchOptions.headers.referer = NODEJS_DEFAULT_REFERER_HEADER; } /* istanbul ignore else blob responses are difficult to make cross platform we will just have to trust the isomorphic fetch will do its job */ if (!requiresFormData(params) && !forceFormData) { fetchOptions.headers["Content-Type"] = "application/x-www-form-urlencoded"; } /** * Check for a global fetch first and use it if available. This allows us to use the default * configuration of fetch-mock in tests. */ /* istanbul ignore next coverage is based on browser code and we don't test for the absence of global fetch so we can skip the else here. */ return globalThis.fetch ? globalThis.fetch(url, fetchOptions) : getFetch().then(({ fetch }) => { return fetch(url, fetchOptions); }); }) .then((response) => { // the request got back an error status code (4xx, 5xx) if (!response.ok) { // we need to determine if the server returned a JSON body with more details. // this is the format used by newer services such as the Places and Style service. return response .json() .then((jsonError) => { // The body can be parsed as JSON const { status, statusText } = response; const { message, details } = jsonError.error; const formattedMessage = `${message}. ${details ? details.join(" ") : ""}`.trim(); throw new ArcGISRequestError(formattedMessage, `HTTP ${status} ${statusText}`, jsonError, url, options); }) .catch((e) => { // if we already were about to format this as an ArcGISRequestError throw that error if (e.name === "ArcGISRequestError") { throw e; } // server responded w/ an actual error (404, 500, etc) but we could not parse it as JSON const { status, statusText } = response; throw new ArcGISRequestError(statusText, `HTTP ${status}`, response, url, options); }); } if (rawResponse) { return response; } switch (params.f) { case "json": return response.json(); case "geojson": return response.json(); case "html": return response.text(); case "text": return response.text(); /* istanbul ignore next blob responses are difficult to make cross platform we will just have to trust that isomorphic fetch will do its job */ default: return response.blob(); } }) .then((data) => { // Check for an error in the JSON body of a successful response. // Most ArcGIS Server services will return a successful status code but include an error in the response body. if ((params.f === "json" || params.f === "geojson") && !rawResponse) { const response = checkForErrors(data, originalUrl, params, options, originalAuthError); if (originalAuthError) { /* If the request was made to an unfederated service that didn't require authentication, add the base url and a dummy token to the list of trusted servers to avoid another federation check in the event of a repeat request */ const truncatedUrl = url .toLowerCase() .split(/\/rest(\/admin)?\/services\//)[0]; options.authentication.federatedServers[truncatedUrl] = { token: [], // default to 24 hours expires: new Date(Date.now() + 86400 * 1000) }; originalAuthError = null; } return response; } else { return data; } }); } /** * Generic method for making HTTP requests to ArcGIS REST API endpoints. * * ```js * import { request } from '@esri/arcgis-rest-request'; * * request('https://www.arcgis.com/sharing/rest') * .then(response) // response.currentVersion === 5.2 * * request('https://www.arcgis.com/sharing/rest', { * httpMethod: "GET" * }) * * request('https://www.arcgis.com/sharing/rest/search', { * params: { q: 'parks' } * }) * .then(response) // response.total => 78379 * ``` * * @param url - The URL of the ArcGIS REST API endpoint. * @param requestOptions - Options for the request, including parameters relevant to the endpoint. * @returns A Promise that will resolve with the data from the response. */ function request(url, requestOptions = { params: { f: "json" } }) { const { request } = requestOptions, internalOptions = __rest(requestOptions, ["request"]); // if the user passed in a custom request function, use that instead of the default return request ? request(url, internalOptions) : internalRequest(url, internalOptions).catch((e) => { if (e instanceof ArcGISAuthError && requestOptions.authentication && typeof requestOptions.authentication !== "string" && requestOptions.authentication.canRefresh && requestOptions.authentication.refreshCredentials) { return e.retry(() => { return requestOptions.authentication.refreshCredentials(); }, 1); } else { return Promise.reject(e); } }); } /* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Helper for methods with lots of first order request options to pass through as request parameters. */ function appendCustomParams(customOptions, keys, baseOptions) { // NOTE: this must be kept in sync with the keys in IRequestOptions const requestOptionsKeys = [ "params", "httpMethod", "rawResponse", "authentication", "hideToken", "portal", "credentials", "maxUrlLength", "headers", "signal", "suppressWarnings", "request" ]; const options = Object.assign(Object.assign({ params: {} }, baseOptions), customOptions); // merge all keys in customOptions into options.params options.params = keys.reduce((value, key) => { if (customOptions[key] || typeof customOptions[key] === "boolean" || (typeof customOptions[key] === "number" && customOptions[key] === 0)) { value[key] = customOptions[key]; } return value; }, options.params); // now remove all properties in options that don't exist in IRequestOptions return requestOptionsKeys.reduce((value, key) => { if (options[key]) { value[key] = options[key]; } return value; }, {}); } /* Copyright (c) 2022 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * There are 5 potential error codes that might be thrown by {@linkcode ArcGISTokenRequestError}. 2 of these codes are used by both * {@linkcode ArcGISIdentityManager} or {@linkcode ApplicationCredentialsManager}: * * * `TOKEN_REFRESH_FAILED` when a request for an new access token fails. * * `UNKNOWN_ERROR_CODE` the error is unknown. More information may be available in {@linkcode ArcGISTokenRequestError.response} * * The 3 remaining error codes will only be thrown when using {@linkcode ArcGISIdentityManager}: * * * `GENERATE_TOKEN_FOR_SERVER_FAILED` when a request for a token for a specific federated server fails. * * `REFRESH_TOKEN_EXCHANGE_FAILED` when a request for a new refresh token fails. * * `NOT_FEDERATED` when the requested server isn't federated with the portal specified in {@linkcode ArcGISIdentityManager.portal}. */ exports.ArcGISTokenRequestErrorCodes = void 0; (function (ArcGISTokenRequestErrorCodes) { ArcGISTokenRequestErrorCodes["TOKEN_REFRESH_FAILED"] = "TOKEN_REFRESH_FAILED"; ArcGISTokenRequestErrorCodes["GENERATE_TOKEN_FOR_SERVER_FAILED"] = "GENERATE_TOKEN_FOR_SERVER_FAILED"; ArcGISTokenRequestErrorCodes["REFRESH_TOKEN_EXCHANGE_FAILED"] = "REFRESH_TOKEN_EXCHANGE_FAILED"; ArcGISTokenRequestErrorCodes["NOT_FEDERATED"] = "NOT_FEDERATED"; ArcGISTokenRequestErrorCodes["UNKNOWN_ERROR_CODE"] = "UNKNOWN_ERROR_CODE"; })(exports.ArcGISTokenRequestErrorCodes || (exports.ArcGISTokenRequestErrorCodes = {})); /** * This error is thrown when {@linkcode ArcGISIdentityManager} or {@linkcode ApplicationCredentialsManager} fails to refresh a token or generate a new token * for a request. Generally in this scenario the credentials are invalid for the request and the you should recreate the {@linkcode ApplicationCredentialsManager} * or prompt the user to authenticate again with {@linkcode ArcGISIdentityManager}. See {@linkcode ArcGISTokenRequestErrorCodes} for a more detailed description of * the possible error codes. * * ```js * request(someUrl, { * authentication: someAuthenticationManager * }).catch(e => { * if(e.name === "ArcGISTokenRequestError") { * // ArcGIS REST JS could not generate an appropriate token for this request * // All credentials are likely invalid and the authentication process should be restarted * } * }) * ``` */ class ArcGISTokenRequestError extends Error { /** * Create a new `ArcGISTokenRequestError` object. * * @param message - The error message from the API * @param code - The error code from the API * @param response - The original response from the API that caused the error * @param url - The original url of the request * @param options - The original options and parameters of the request */ constructor(message = "UNKNOWN_ERROR", code = exports.ArcGISTokenRequestErrorCodes.UNKNOWN_ERROR_CODE, response, url, options) { // 'Error' breaks prototype chain here super(message); // restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class // we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now. // Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work // and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); this.name = "ArcGISTokenRequestError"; this.message = `${code}: ${message}`; this.originalMessage = message; this.code = code; this.response = response; this.url = url; this.options = options; } } /* Copyright (c) 2022 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * This error code will be thrown by the following methods when the user cancels or denies an authorization request on the OAuth 2.0 * authorization screen. * * * {@linkcode ArcGISIdentityManager.beginOAuth2} when the `popup` option is `true` * * {@linkcode ArcGISIdentityManager.completeOAuth2} when the `popup` option is `false` * * ```js * import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; * * ArcGISIdentityManager.beginOAuth2({ * clientId: "***" * redirectUri: "***", * popup: true * }).then(authenticationManager => { * console.log("OAuth 2.0 Successful"); * }).catch(e => { * if(e.name === "ArcGISAccessDeniedError") { * console.log("The user did not authorize your app.") * } else { * console.log("Something else went wrong. Error:", e); * } * }) * ``` */ class ArcGISAccessDeniedError extends Error { /** * Create a new `ArcGISAccessDeniedError` object. */ constructor() { const message = "The user has denied your authorization request."; super(message); // restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class // we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now. // Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work // and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); this.name = "ArcGISAccessDeniedError"; } } /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * This represents a generic error from a {@linkcode Job}. There will be details about the error in the {@linkcode ArcGISJobError.jobInfo}. * * ```js * job.getAllResults().catch(e => { * if(e.name === "ArcGISJobError") { * console.log("Something went wrong with the job", e); * console.log("Full job info", e.jobInfo); * } * }) * ``` */ class ArcGISJobError extends Error { /** * Create a new `ArcGISJobError` object. * * @param message - The error message from the API * @param jobInfo - The info of the job that is in an error state */ constructor(message = "Unknown error", jobInfo) { // 'Error' breaks prototype chain here super(message); // restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class // we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now. // Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work // and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); this.name = "ArcGISJobError"; this.message = `${jobInfo.status}: ${message}`; this.status = jobInfo.status; this.id = jobInfo.id; this.jobInfo = jobInfo; } } /* Copyright (c) 2018 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Helper method to ensure that user supplied urls don't include whitespace or a trailing slash. */ function cleanUrl(url) { // Guard so we don't try to trim something that's not a string if (typeof url !== "string") { return url; } // trim leading and trailing spaces, but not spaces inside the url url = url.trim(); // remove the trailing slash to the url if one was included if (url[url.length - 1] === "/") { url = url.slice(0, -1); } return url; } /* Copyright (c) 2017-2020 Environmental Systems Research Institute, Inc. * Apache-2.0 */ function decodeParam(param) { const [key, value] = param.split("="); return { key: decodeURIComponent(key), value: decodeURIComponent(value) }; } /** * Decodes the passed query string as an object. * * @param query A string to be decoded. * @returns A decoded query param object. */ function decodeQueryString(query) { if (!query || query.length <= 0) { return {}; } return query .replace(/^#/, "") .replace(/^\?/, "") .split("&") .reduce((acc, entry) => { const { key, value } = decodeParam(entry); acc[key] = value; return acc; }, {}); } /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ /** * Enum describing the different errors that might be thrown by a request. * * ```ts * import { request, ErrorTypes } from '@esri/arcgis-rest-request'; * * request("...").catch((e) => { * switch(e.name) { * case ErrorType.ArcGISRequestError: * // handle a general error from the API * break; * * case ErrorType.ArcGISAuthError: * // handle an authentication error * break; * * case ErrorType.ArcGISAccessDeniedError: * // handle a user denying an authorization request in an oAuth workflow * break; * * default: * // handle some other error (usually a network error) * } * }); * ``` */ exports.ErrorTypes = void 0; (function (ErrorTypes) { ErrorTypes["ArcGISRequestError"] = "ArcGISRequestError"; ErrorTypes["ArcGISAuthError"] = "ArcGISAuthError"; ErrorTypes["ArcGISAccessDeniedError"] = "ArcGISAccessDeniedError"; ErrorTypes["ArcGISTokenRequestError"] = "ArcGISTokenRequestError"; })(exports.ErrorTypes || (exports.ErrorTypes = {})); /* Copyright (c) 2017 Environmental Systems Research Institute, Inc. * Apache-2.0 */ const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; function fetchToken(url, requestOptions) { const options = requestOptions; // we generate a response, so we can't return the raw response options.rawResponse = false; return request(url, options).then((response) => { // Typescript uses the "in" keyword to determine we have a generateToken response or an oauth token response if ("token" in response && "expires" in response) { return { token: response.token, username: requestOptions.params.username, expires: new Date(response.expires) }; } const portalTokenResponse = { token: response.access_token, username: response.username, expires: new Date( // convert seconds in response to milliseconds and add the value to the current time to calculate a static expiration timestamp // we subtract 5 minutes here to make sure that we refresh the token early if the user makes requests Date.now() + response.expires_in * 1000 - FIVE_MINUTES_IN_MILLISECONDS), ssl: response.ssl === true }; if (response.refresh_token) { portalTokenResponse.refreshToken = response.refresh_token; } if (response.refresh_token_expires_in) { portalTokenResponse.refreshTokenExpires = new Date( // convert seconds in response to milliseconds and add the value to the current time to calculate a static expiration timestamp // we subtract 5 minutes here to make sure that we refresh the token early if the user makes requests Date.now() + response.refresh_token_expires_in * 1000 - FIVE_MINUTES_IN_MILLISECONDS); } return portalTokenResponse; }); } class AuthenticationManagerBase { constructor(options) { this.portal = options.portal ? cleanUrl(options.portal) : "https://www.arcgis.com/sharing/rest"; this._username = options.username; } /** * The username of the currently authenticated user. */ get username() { if (this._username) { return this._username; } if (this._user && this._user.username) { return this._user.username; } } /** * Returns the username for the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. This is also used internally when a username is required for some requests but is not present in the options. * * ```js * manager.getUsername() * .then(response => { * console.log(response); // "casey_jones" * }) * ``` */ getUsername() { if (this.username) { return Promise.resolve(this.username); } else { return this.getUser().then((user) => { return user.username; }); } } /** * Returns information about the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. * * ```js * manager.getUser() * .then(response => { * console.log(response.role); // "org_admin" * }) * ``` * * @param requestOptions - Options for the request. NOTE: `rawResponse` is not supported by this operation. * @returns A Promise that will resolve with the data from the response. */ getUser(requestOptions) { if (this._pendingUserRequest) {