diff --git a/src/base/BaseTwilio.ts b/src/base/BaseTwilio.ts index 8cac1826d..3020bc9c9 100644 --- a/src/base/BaseTwilio.ts +++ b/src/base/BaseTwilio.ts @@ -1,6 +1,8 @@ import RequestClient from "./RequestClient"; /* jshint ignore:line */ import { HttpMethod } from "../interfaces"; /* jshint ignore:line */ import { Headers } from "../http/request"; /* jshint ignore:line */ +import AuthStrategy from "../auth_strategy/AuthStrategy"; /* jshint ignore:line */ +import CredentialProvider from "../credential_provider/CredentialProvider"; /* jshint ignore:line */ const os = require("os"); /* jshint ignore:line */ const url = require("url"); /* jshint ignore:line */ @@ -40,6 +42,7 @@ namespace Twilio { uri?: string; username?: string; password?: string; + authStrategy?: AuthStrategy; headers?: Headers; params?: object; data?: object; @@ -56,9 +59,10 @@ namespace Twilio { /* jshint ignore:end */ export class Client { - username: string; - password: string; + username?: string; + password?: string; accountSid: string; + credentialProvider?: CredentialProvider; opts?: ClientOpts; env?: NodeJS.ProcessEnv; edge?: string; @@ -101,23 +105,23 @@ namespace Twilio { /* jshint ignore:end */ constructor(username?: string, password?: string, opts?: ClientOpts) { - this.opts = opts || {}; - this.env = this.opts.env || {}; + this.setOpts(opts); this.username = username ?? - this.env.TWILIO_ACCOUNT_SID ?? - process.env.TWILIO_ACCOUNT_SID ?? - (() => { - throw new Error("username is required"); - })(); + this.env?.TWILIO_ACCOUNT_SID ?? + process.env.TWILIO_ACCOUNT_SID; this.password = password ?? - this.env.TWILIO_AUTH_TOKEN ?? - process.env.TWILIO_AUTH_TOKEN ?? - (() => { - throw new Error("password is required"); - })(); - this.accountSid = this.opts.accountSid || this.username; + this.env?.TWILIO_AUTH_TOKEN ?? + process.env.TWILIO_AUTH_TOKEN; + this.accountSid = ""; + this.setAccountSid(this.opts?.accountSid || this.username); + this.invalidateOAuth(); + } + + setOpts(opts?: ClientOpts) { + this.opts = opts || {}; + this.env = this.opts.env || {}; this.edge = this.opts.edge ?? this.env.TWILIO_EDGE ?? process.env.TWILIO_EDGE; this.region = @@ -144,9 +148,13 @@ namespace Twilio { if (this.opts.lazyLoading === false) { this._httpClient = this.httpClient; } + } - if (!this.accountSid.startsWith("AC")) { - const apiKeyMsg = this.accountSid.startsWith("SK") + setAccountSid(accountSid?: string) { + this.accountSid = accountSid || ""; + + if (this.accountSid && !this.accountSid?.startsWith("AC")) { + const apiKeyMsg = this.accountSid?.startsWith("SK") ? ". The given SID indicates an API Key which requires the accountSid to be passed as an additional option" : ""; @@ -154,6 +162,21 @@ namespace Twilio { } } + setCredentialProvider(credentialProvider: CredentialProvider) { + this.credentialProvider = credentialProvider; + this.accountSid = ""; + this.invalidateBasicAuth(); + } + + invalidateBasicAuth() { + this.username = undefined; + this.password = undefined; + } + + invalidateOAuth() { + this.credentialProvider = undefined; + } + get httpClient() { if (!this._httpClient) { this._httpClient = new RequestClient({ @@ -196,6 +219,22 @@ namespace Twilio { const username = opts.username || this.username; const password = opts.password || this.password; + const authStrategy = + opts.authStrategy || this.credentialProvider?.toAuthStrategy(); + + if (!authStrategy) { + if (!username) { + (() => { + throw new Error("username is required"); + })(); + } + + if (!password) { + (() => { + throw new Error("password is required"); + })(); + } + } const headers = opts.headers || {}; @@ -223,7 +262,7 @@ namespace Twilio { headers["Content-Type"] = "application/x-www-form-urlencoded"; } - if (!headers["Accept"]) { + if (opts.method !== "delete" && !headers["Accept"]) { headers["Accept"] = "application/json"; } @@ -235,6 +274,7 @@ namespace Twilio { uri: uri.href, username: username, password: password, + authStrategy: authStrategy, headers: headers, params: opts.params, data: opts.data, diff --git a/src/base/Page.ts b/src/base/Page.ts index 2230e4715..e2062e270 100644 --- a/src/base/Page.ts +++ b/src/base/Page.ts @@ -249,6 +249,8 @@ export default class Page< if (keys.length === 1) { return payload[keys[0]]; } + for (const key of keys) + if (Array.isArray(payload[key])) return payload[key]; throw new Error("Page Records cannot be deserialized"); } diff --git a/src/base/RequestClient.ts b/src/base/RequestClient.ts index 02c9134fe..4fb29bb53 100644 --- a/src/base/RequestClient.ts +++ b/src/base/RequestClient.ts @@ -6,9 +6,10 @@ import qs from "qs"; import * as https from "https"; import Response from "../http/response"; import Request, { - RequestOptions as LastRequestOptions, Headers, + RequestOptions as LastRequestOptions, } from "../http/request"; +import AuthStrategy from "../auth_strategy/AuthStrategy"; const DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded"; const DEFAULT_TIMEOUT = 30000; @@ -149,6 +150,7 @@ class RequestClient { * @param opts.uri - The request uri * @param opts.username - The username used for auth * @param opts.password - The password used for auth + * @param opts.authStrategy - The authStrategy for API call * @param opts.headers - The request headers * @param opts.params - The request params * @param opts.data - The request data @@ -157,7 +159,7 @@ class RequestClient { * @param opts.forever - Set to true to use the forever-agent * @param opts.logLevel - Show debug logs */ - request( + async request( opts: RequestClient.RequestOptions ): Promise> { if (!opts.method) { @@ -180,6 +182,8 @@ class RequestClient { "base64" ); headers.Authorization = "Basic " + auth; + } else if (opts.authStrategy) { + headers.Authorization = await opts.authStrategy.getAuthString(); } const options: AxiosRequestConfig = { @@ -296,6 +300,10 @@ namespace RequestClient { * The password used for auth */ password?: string; + /** + * The AuthStrategy for API Call + */ + authStrategy?: AuthStrategy; /** * The request headers */ diff --git a/src/index.ts b/src/index.ts index 1d22dba6a..401c508d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,8 @@ import * as taskRouterUtil from "./jwt/taskrouter/util"; import IVoiceResponse from "./twiml/VoiceResponse"; import IMessagingResponse from "./twiml/MessagingResponse"; import IFaxResponse from "./twiml/FaxResponse"; +import IClientCredentialProvider from "./credential_provider/ClientCredentialProvider"; +import INoAuthCredentialProvider from "./credential_provider/NoAuthCredentialProvider"; // Shorthand to automatically create a RestClient function TwilioSDK( @@ -44,6 +46,17 @@ namespace TwilioSDK { } export type RequestClient = IRequestClient; export const RequestClient = IRequestClient; + + export type ClientCredentialProviderBuilder = + IClientCredentialProvider.ClientCredentialProviderBuilder; + export const ClientCredentialProviderBuilder = + IClientCredentialProvider.ClientCredentialProviderBuilder; + + export type NoAuthCredentialProvider = + INoAuthCredentialProvider.NoAuthCredentialProvider; + export const NoAuthCredentialProvider = + INoAuthCredentialProvider.NoAuthCredentialProvider; + // Setup webhook helper functionality export type validateBody = typeof webhooks.validateBody; export const validateBody = webhooks.validateBody;