From 049757fa19f8920a87eed1c9b28ba862b4145cde Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Tue, 24 Sep 2024 12:55:04 -0700 Subject: [PATCH 01/20] Support new Usage Billing APIs --- src/Error.ts | 31 +- src/RequestSender.ts | 333 +++++++++++------- src/StripeResource.ts | 15 +- src/Types.d.ts | 59 +++- src/Webhooks.ts | 2 +- src/apiVersion.ts | 1 + src/autoPagination.ts | 83 ++++- src/crypto/CryptoProvider.ts | 7 + src/crypto/NodeCryptoProvider.ts | 10 + src/crypto/SubtleCryptoProvider.ts | 5 + src/multipart.ts | 4 +- src/net/FetchHttpClient.ts | 2 +- src/net/HttpClient.ts | 4 +- src/net/NodeHttpClient.ts | 2 +- src/resources.ts | 1 + src/resources/OAuth.ts | 4 +- src/resources/V2.ts | 12 + src/resources/V2/Billing.ts | 16 + .../V2/Billing/MeterEventAdjustments.ts | 10 + src/resources/V2/Billing/MeterEventSession.ts | 10 + src/resources/V2/Billing/MeterEventStream.ts | 11 + src/resources/V2/Billing/MeterEvents.ts | 7 + src/resources/V2/Core.ts | 10 + src/resources/V2/Core/Events.ts | 50 +++ src/stripe.core.ts | 186 +++++++++- src/utils.ts | 80 ++++- test/Error.spec.ts | 24 ++ test/RequestSender.spec.ts | 235 +++++++++++- test/autoPagination.spec.ts | 102 +++++- test/crypto/helpers.ts | 11 + test/net/helpers.ts | 2 +- .../resources/generated_examples_test.spec.js | 13 + test/stripe.spec.ts | 295 +++++++++++++++- test/testUtils.ts | 25 +- test/utils.spec.ts | 57 +-- types/Errors.d.ts | 34 +- types/EventTypes.d.ts | 2 + types/V2/Billing/MeterEventAdjustmentV2S.d.ts | 65 ++++ .../MeterEventAdjustmentsResource.d.ts | 47 +++ .../V2/Billing/MeterEventSessionResource.d.ts | 26 ++ types/V2/Billing/MeterEventSessions.d.ts | 45 +++ .../V2/Billing/MeterEventStreamResource.d.ts | 62 ++++ types/V2/Billing/MeterEventV2S.d.ts | 54 +++ types/V2/Billing/MeterEventsResource.d.ts | 52 +++ types/V2/BillingResource.d.ts | 14 + types/V2/Core/EventsResource.d.ts | 49 +++ types/V2/CoreResource.d.ts | 11 + types/V2/EventDestinations.d.ts | 154 ++++++++ types/V2/EventTypes.d.ts | 224 ++++++++++++ types/V2/Events.d.ts | 75 ++++ types/V2Resource.d.ts | 10 + types/index.d.ts | 14 + 52 files changed, 2422 insertions(+), 235 deletions(-) create mode 100644 src/resources/V2.ts create mode 100644 src/resources/V2/Billing.ts create mode 100644 src/resources/V2/Billing/MeterEventAdjustments.ts create mode 100644 src/resources/V2/Billing/MeterEventSession.ts create mode 100644 src/resources/V2/Billing/MeterEventStream.ts create mode 100644 src/resources/V2/Billing/MeterEvents.ts create mode 100644 src/resources/V2/Core.ts create mode 100644 src/resources/V2/Core/Events.ts create mode 100644 types/V2/Billing/MeterEventAdjustmentV2S.d.ts create mode 100644 types/V2/Billing/MeterEventAdjustmentsResource.d.ts create mode 100644 types/V2/Billing/MeterEventSessionResource.d.ts create mode 100644 types/V2/Billing/MeterEventSessions.d.ts create mode 100644 types/V2/Billing/MeterEventStreamResource.d.ts create mode 100644 types/V2/Billing/MeterEventV2S.d.ts create mode 100644 types/V2/Billing/MeterEventsResource.d.ts create mode 100644 types/V2/BillingResource.d.ts create mode 100644 types/V2/Core/EventsResource.d.ts create mode 100644 types/V2/CoreResource.d.ts create mode 100644 types/V2/EventDestinations.d.ts create mode 100644 types/V2/EventTypes.d.ts create mode 100644 types/V2/Events.d.ts create mode 100644 types/V2Resource.d.ts diff --git a/src/Error.ts b/src/Error.ts index aa094c0952..b52aeeac5e 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -1,4 +1,5 @@ /* eslint-disable camelcase */ +/* eslint-disable no-warning-comments */ import {RawErrorType, StripeRawError} from './Types.js'; @@ -23,12 +24,32 @@ export const generate = (rawStripeError: StripeRawError): StripeError => { } }; +// eslint-disable-next-line complexity +export const generateV2 = (rawStripeError: StripeRawError): StripeError => { + switch (rawStripeError.type) { + // switchCases: The beginning of the section generated from our OpenAPI spec + case 'temporary_session_expired': + return new TemporarySessionExpiredError(rawStripeError); + // switchCases: The end of the section generated from our OpenAPI spec + } + + // Special handling for requests with missing required fields in V2 APIs. + // invalid_field response in V2 APIs returns the field 'code' instead of 'type'. + switch (rawStripeError.code) { + case 'invalid_fields': + return new StripeInvalidRequestError(rawStripeError); + } + + return generate(rawStripeError); +}; + /** * StripeError is the base error from which all other more specific Stripe errors derive. * Specifically for errors returned from Stripe's REST API. */ export class StripeError extends Error { readonly message: string; + readonly userMessage?: string; readonly type: string; readonly raw: unknown; readonly rawType?: RawErrorType; @@ -64,7 +85,7 @@ export class StripeError extends Error { this.statusCode = raw.statusCode; // @ts-ignore this.message = raw.message; - + this.userMessage = raw.user_message; this.charge = raw.charge; this.decline_code = raw.decline_code; this.payment_intent = raw.payment_intent; @@ -205,3 +226,11 @@ export class StripeUnknownError extends StripeError { super(raw, 'StripeUnknownError'); } } + +// classDefinitions: The beginning of the section generated from our OpenAPI spec +export class TemporarySessionExpiredError extends StripeError { + constructor(rawStripeError: StripeRawError = {}) { + super(rawStripeError, 'TemporarySessionExpiredError'); + } +} +// classDefinitions: The end of the section generated from our OpenAPI spec diff --git a/src/RequestSender.ts b/src/RequestSender.ts index d69c3c0b87..7c73a17270 100644 --- a/src/RequestSender.ts +++ b/src/RequestSender.ts @@ -1,3 +1,4 @@ +import * as Errors from './Error.js'; import { StripeAPIError, StripeAuthenticationError, @@ -7,24 +8,30 @@ import { StripeRateLimitError, } from './Error.js'; import { - emitWarning, - normalizeHeaders, - removeNullish, - stringifyRequestData, -} from './utils.js'; -import {HttpClient, HttpClientResponseInterface} from './net/HttpClient.js'; -import { - StripeObject, - RequestHeaders, - RequestEvent, - ResponseEvent, + ApiMode, + RequestAuthenticator, RequestCallback, RequestCallbackReturn, - RequestSettings, RequestData, - RequestOptions, RequestDataProcessor, + RequestEvent, + RequestHeaders, + RequestOptions, + RequestSettings, + ResponseEvent, + StripeObject, + StripeRequest, } from './Types.js'; +import {PreviewVersion} from './apiVersion.js'; +import {HttpClient, HttpClientResponseInterface} from './net/HttpClient.js'; +import { + emitWarning, + jsonStringifyRequestData, + normalizeHeaders, + queryStringifyRequestData, + removeNullish, + getAPIMode, +} from './utils.js'; export type HttpClientResponseError = {code: string}; @@ -126,6 +133,7 @@ export class RequestSender { */ _jsonResponseHandler( requestEvent: RequestEvent, + apiMode: 'v1' | 'v2', usage: Array, callback: RequestCallback ) { @@ -167,8 +175,10 @@ export class RequestSender { err = new StripePermissionError(jsonResponse.error); } else if (statusCode === 429) { err = new StripeRateLimitError(jsonResponse.error); + } else if (apiMode === 'v2') { + err = Errors.generateV2(jsonResponse.error); } else { - err = StripeError.generate(jsonResponse.error); + err = Errors.generate(jsonResponse.error); } throw err; @@ -272,7 +282,7 @@ export class RequestSender { // number of numRetries so far as inputs. Do not allow the number to exceed // maxNetworkRetryDelay. let sleepSeconds = Math.min( - initialNetworkRetryDelay * Math.pow(numRetries - 1, 2), + initialNetworkRetryDelay * Math.pow(2, numRetries - 1), maxNetworkRetryDelay ); @@ -301,39 +311,65 @@ export class RequestSender { _defaultIdempotencyKey( method: string, - settings: RequestSettings + settings: RequestSettings, + apiMode: ApiMode ): string | null { // If this is a POST and we allow multiple retries, ensure an idempotency key. const maxRetries = this._getMaxNetworkRetries(settings); - if (method === 'POST' && maxRetries > 0) { - return `stripe-node-retry-${this._stripe._platformFunctions.uuid4()}`; + const genKey = (): string => + `stripe-node-retry-${this._stripe._platformFunctions.uuid4()}`; + + // more verbose than it needs to be, but gives clear separation between V1 and V2 behavior + if (apiMode === 'v2') { + if (method === 'POST' || method === 'DELETE') { + return genKey(); + } + } else if (apiMode === 'v1') { + if (method === 'POST' && maxRetries > 0) { + return genKey(); + } } + return null; } - _makeHeaders( - auth: string | null, - contentLength: number, - apiVersion: string, - clientUserAgent: string, - method: string, - userSuppliedHeaders: RequestHeaders | null, - userSuppliedSettings: RequestSettings - ): RequestHeaders { + _makeHeaders({ + contentType, + contentLength, + apiVersion, + clientUserAgent, + method, + userSuppliedHeaders, + userSuppliedSettings, + stripeAccount, + stripeContext, + apiMode, + }: { + contentType: string; + contentLength: number; + apiVersion: string | null; + clientUserAgent: string; + method: string; + userSuppliedHeaders: RequestHeaders | null; + userSuppliedSettings: RequestSettings; + stripeAccount: string | null; + stripeContext: string | null; + apiMode: ApiMode; + }): RequestHeaders { const defaultHeaders = { - // Use specified auth token or use default from this stripe instance: - Authorization: auth ? `Bearer ${auth}` : this._stripe.getApiField('auth'), Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': this._getUserAgentString(), + 'Content-Type': contentType, + 'User-Agent': this._getUserAgentString(apiMode), 'X-Stripe-Client-User-Agent': clientUserAgent, 'X-Stripe-Client-Telemetry': this._getTelemetryHeader(), 'Stripe-Version': apiVersion, - 'Stripe-Account': this._stripe.getApiField('stripeAccount'), + 'Stripe-Account': stripeAccount, + 'Stripe-Context': stripeContext, 'Idempotency-Key': this._defaultIdempotencyKey( method, - userSuppliedSettings + userSuppliedSettings, + apiMode ), } as RequestHeaders; @@ -372,13 +408,13 @@ export class RequestSender { ); } - _getUserAgentString(): string { + _getUserAgentString(apiMode: string): string { const packageVersion = this._stripe.getConstant('PACKAGE_VERSION'); const appInfo = this._stripe._appInfo ? this._stripe.getAppInfoAsString() : ''; - return `Stripe/v1 NodeBindings/${packageVersion} ${appInfo}`.trim(); + return `Stripe/${apiMode} NodeBindings/${packageVersion} ${appInfo}`.trim(); } _getTelemetryHeader(): string | undefined { @@ -427,14 +463,15 @@ export class RequestSender { host: string | null, path: string, data: RequestData, - auth: string | null, - options: RequestOptions = {}, + authenticator: RequestAuthenticator, + options: RequestOptions, usage: Array = [], callback: RequestCallback, requestDataProcessor: RequestDataProcessor | null = null ): void { let requestData: string; - + authenticator = authenticator ?? this._stripe._authenticator; + const apiMode: ApiMode = getAPIMode(path); const retryRequest = ( requestFn: typeof makeRequest, apiVersion: string, @@ -465,88 +502,113 @@ export class RequestSender { ? options.settings.timeout : this._stripe.getApiField('timeout'); - const req = this._stripe - .getApiField('httpClient') - .makeRequest( - host || this._stripe.getApiField('host'), - this._stripe.getApiField('port'), - path, - method, - headers, - requestData, - this._stripe.getApiField('protocol'), - timeout - ); - - const requestStartTime = Date.now(); - - // @ts-ignore - const requestEvent: RequestEvent = removeNullish({ - api_version: apiVersion, - account: headers['Stripe-Account'], - idempotency_key: headers['Idempotency-Key'], - method, - path, - request_start_time: requestStartTime, - }); + const request = { + host: host || this._stripe.getApiField('host'), + port: this._stripe.getApiField('port'), + path: path, + method: method, + headers: Object.assign({}, headers), + body: requestData, + protocol: this._stripe.getApiField('protocol'), + }; - const requestRetries = numRetries || 0; - - const maxRetries = this._getMaxNetworkRetries(options.settings || {}); - this._stripe._emitter.emit('request', requestEvent); - - req - .then((res: HttpClientResponseInterface) => { - if (RequestSender._shouldRetry(res, requestRetries, maxRetries)) { - return retryRequest( - makeRequest, - apiVersion, - headers, - requestRetries, - // @ts-ignore - res.getHeaders()['retry-after'] + authenticator(request) + .then(() => { + const req = this._stripe + .getApiField('httpClient') + .makeRequest( + request.host, + request.port, + request.path, + request.method, + request.headers, + request.body, + request.protocol, + timeout ); - } else if (options.streaming && res.getStatusCode() < 400) { - return this._streamingResponseHandler( - requestEvent, - usage, - callback - )(res); - } else { - return this._jsonResponseHandler( - requestEvent, - usage, - callback - )(res); - } + + const requestStartTime = Date.now(); + + // @ts-ignore + const requestEvent: RequestEvent = removeNullish({ + api_version: apiVersion, + account: headers['Stripe-Account'], + idempotency_key: headers['Idempotency-Key'], + method, + path, + request_start_time: requestStartTime, + }); + + const requestRetries = numRetries || 0; + + const maxRetries = this._getMaxNetworkRetries(options.settings || {}); + this._stripe._emitter.emit('request', requestEvent); + + req + .then((res: HttpClientResponseInterface) => { + if (RequestSender._shouldRetry(res, requestRetries, maxRetries)) { + return retryRequest( + makeRequest, + apiVersion, + headers, + requestRetries, + // @ts-ignore + res.getHeaders()['retry-after'] + ); + } else if (options.streaming && res.getStatusCode() < 400) { + return this._streamingResponseHandler( + requestEvent, + usage, + callback + )(res); + } else { + return this._jsonResponseHandler( + requestEvent, + apiMode, + usage, + callback + )(res); + } + }) + .catch((error: HttpClientResponseError) => { + if ( + RequestSender._shouldRetry( + null, + requestRetries, + maxRetries, + error + ) + ) { + return retryRequest( + makeRequest, + apiVersion, + headers, + requestRetries, + null + ); + } else { + const isTimeoutError = + error.code && error.code === HttpClient.TIMEOUT_ERROR_CODE; + + return callback( + new StripeConnectionError({ + message: isTimeoutError + ? `Request aborted due to timeout being reached (${timeout}ms)` + : RequestSender._generateConnectionErrorMessage( + requestRetries + ), + // @ts-ignore + detail: error, + }) + ); + } + }); }) - .catch((error: HttpClientResponseError) => { - if ( - RequestSender._shouldRetry(null, requestRetries, maxRetries, error) - ) { - return retryRequest( - makeRequest, - apiVersion, - headers, - requestRetries, - null - ); - } else { - const isTimeoutError = - error.code && error.code === HttpClient.TIMEOUT_ERROR_CODE; - - return callback( - new StripeConnectionError({ - message: isTimeoutError - ? `Request aborted due to timeout being reached (${timeout}ms)` - : RequestSender._generateConnectionErrorMessage( - requestRetries - ), - // @ts-ignore - detail: error, - }) - ); - } + .catch((e) => { + throw new StripeError({ + message: 'Unable to authenticate the request', + exception: e, + }); }); }; @@ -558,16 +620,27 @@ export class RequestSender { requestData = data; this._stripe.getClientUserAgent((clientUserAgent: string) => { - const apiVersion = this._stripe.getApiField('version'); - const headers = this._makeHeaders( - auth, - requestData.length, - apiVersion, + const apiVersion = + apiMode == 'v2' + ? PreviewVersion + : this._stripe.getApiField('version'); + const headers = this._makeHeaders({ + contentType: + apiMode == 'v2' + ? 'application/json' + : 'application/x-www-form-urlencoded', + contentLength: requestData.length, + apiVersion: apiVersion, clientUserAgent, method, - options.headers ?? null, - options.settings ?? {} - ); + userSuppliedHeaders: options.headers, + userSuppliedSettings: options.settings, + stripeAccount: + apiMode == 'v2' ? null : this._stripe.getApiField('stripeAccount'), + stripeContext: + apiMode == 'v2' ? this._stripe.getApiField('stripeContext') : null, + apiMode: apiMode, + }); makeRequest(apiVersion, headers, 0); }); @@ -581,7 +654,15 @@ export class RequestSender { prepareAndMakeRequest ); } else { - prepareAndMakeRequest(null, stringifyRequestData(data || {})); + let stringifiedData: string; + + if (apiMode == 'v2') { + stringifiedData = data ? jsonStringifyRequestData(data) : ''; + } else { + stringifiedData = queryStringifyRequestData(data || {}, apiMode); + } + + prepareAndMakeRequest(null, stringifiedData); } } } diff --git a/src/StripeResource.ts b/src/StripeResource.ts index 59dc3fbe77..12da1fa007 100644 --- a/src/StripeResource.ts +++ b/src/StripeResource.ts @@ -3,7 +3,8 @@ import { getOptionsFromArgs, makeURLInterpolator, protoExtend, - stringifyRequestData, + queryStringifyRequestData, + getAPIMode, } from './utils.js'; import {stripeMethod} from './StripeMethod.js'; import { @@ -186,7 +187,7 @@ StripeResource.prototype = { requestPath, bodyData, queryData, - auth: options.auth, + authenticator: options.authenticator ?? null, headers, host: host ?? null, streaming, @@ -228,7 +229,7 @@ StripeResource.prototype = { const path = [ opts.requestPath, emptyQuery ? '' : '?', - stringifyRequestData(opts.queryData), + queryStringifyRequestData(opts.queryData, getAPIMode(opts.requestPath)), ].join(''); const {headers, settings} = opts; @@ -238,8 +239,12 @@ StripeResource.prototype = { opts.host, path, opts.bodyData, - opts.auth, - {headers, settings, streaming: opts.streaming}, + opts.authenticator, + { + headers, + settings, + streaming: opts.streaming, + }, opts.usage, requestCallback, this.requestDataProcessor?.bind(this) diff --git a/src/Types.d.ts b/src/Types.d.ts index 6bb97a4ff1..8b94aa6d9f 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -1,12 +1,15 @@ /* eslint-disable camelcase */ import {EventEmitter} from 'events'; +import {CryptoProvider} from './crypto/CryptoProvider.js'; import { HttpClientInterface, HttpClientResponseInterface, } from './net/HttpClient.js'; import {PlatformFunctions} from './platform/PlatformFunctions.js'; +import {WebhookEvent} from './Webhooks.js'; export type AppInfo = {name?: string} & Record; +export type ApiMode = 'v1' | 'v2'; export type BufferedFile = { name: string; type: string; @@ -27,6 +30,7 @@ export type MethodSpec = { usage?: Array; }; export type MultipartRequestData = RequestData | StreamingFile | BufferedFile; +// rawErrorTypeEnum: The beginning of the section generated from our OpenAPI spec export type RawErrorType = | 'card_error' | 'invalid_request_error' @@ -34,8 +38,20 @@ export type RawErrorType = | 'idempotency_error' | 'rate_limit_error' | 'authentication_error' - | 'invalid_grant'; + | 'invalid_grant' + | 'temporary_session_expired'; +// rawErrorTypeEnum: The end of the section generated from our OpenAPI spec export type RequestArgs = Array; +export type StripeRequest = { + host: string; + port: string; + path: string; + method: string; + headers: RequestHeaders; + body: string; + protocol: string; +}; +export type RequestAuthenticator = (request: StripeRequest) => Promise; export type RequestCallback = ( this: void, error: Error | null, @@ -54,16 +70,16 @@ export type RequestEvent = { }; export type RequestHeaders = Record; export type RequestOptions = { - settings?: RequestSettings; - streaming?: boolean; - headers?: RequestHeaders; + settings: RequestSettings; + streaming: boolean; + headers: RequestHeaders; }; export type RequestOpts = { + authenticator: RequestAuthenticator | null; requestMethod: string; requestPath: string; bodyData: RequestData | null; queryData: RequestData; - auth: string | null; headers: RequestHeaders; host: string | null; streaming: boolean; @@ -122,15 +138,29 @@ export type StripeObject = { StripeResource: StripeResourceConstructor; errors: any; webhooks: any; + parseThinEvent: ( + payload: string | Uint8Array, + header: string | Uint8Array, + secret: string, + tolerance?: number, + cryptoProvider?: CryptoProvider, + receivedAt?: number + ) => WebhookEvent; + parseSnapshotEvent: ( + payload: string | Uint8Array, + header: string | Uint8Array, + secret: string, + tolerance?: number, + cryptoProvider?: CryptoProvider, + receivedAt?: number + ) => WebhookEvent; _prepResources: () => void; _setAppInfo: (appInfo: AppInfo) => void; - _setApiKey: (apiKey: string) => void; _prevRequestMetrics: Array<{ request_id: string; request_duration_ms: number; }>; _api: { - auth: string | null; host: string; port: string | number; protocol: string; @@ -139,16 +169,22 @@ export type StripeObject = { timeout: number; maxNetworkRetries: number; agent: string; - httpClient: any; + httpClient: HttpClientInterface; dev: boolean; stripeAccount: string | null; + stripeContext: string | null; }; + _authenticator?: RequestAuthenticator; _emitter: EventEmitter; _enableTelemetry: boolean; _requestSender: RequestSender; _getPropsFromConfig: (config: Record) => UserProvidedConfig; _clientId?: string; _platformFunctions: PlatformFunctions; + _setAuthenticator: ( + key: string, + authenticator: RequestAuthenticator | undefined + ) => void; }; export type RequestSender = { _request( @@ -156,7 +192,7 @@ export type RequestSender = { host: string | null, path: string, data: RequestData | null, - auth: string | null, + authenticator: RequestAuthenticator | null, options: RequestOptions, usage: Array, callback: RequestCallback, @@ -165,6 +201,7 @@ export type RequestSender = { }; export type StripeRawError = { message?: string; + user_message?: string; type?: RawErrorType; headers?: {[header: string]: string}; statusCode?: number; @@ -211,12 +248,13 @@ export type StripeResourceObject = { }; export type RequestDataProcessor = ( method: string, - data: RequestData, + data: RequestData | null, headers: RequestHeaders | undefined, prepareAndMakeRequest: (error: Error | null, data: string) => void ) => void; export type UrlInterpolator = (params: Record) => string; export type UserProvidedConfig = { + authenticator?: RequestAuthenticator; apiVersion?: string; protocol?: string; host?: string; @@ -226,6 +264,7 @@ export type UserProvidedConfig = { maxNetworkRetries?: number; httpClient?: HttpClientInterface; stripeAccount?: string; + stripeContext?: string; typescript?: boolean; telemetry?: boolean; appInfo?: AppInfo; diff --git a/src/Webhooks.ts b/src/Webhooks.ts index fc3dec8907..e0ffa5ea5c 100644 --- a/src/Webhooks.ts +++ b/src/Webhooks.ts @@ -25,7 +25,7 @@ type WebhookTestHeaderOptions = { cryptoProvider: CryptoProvider; }; -type WebhookEvent = Record; +export type WebhookEvent = Record; type WebhookPayload = string | Uint8Array; type WebhookSignatureObject = { verifyHeader: ( diff --git a/src/apiVersion.ts b/src/apiVersion.ts index 1966b582c5..a59577235b 100644 --- a/src/apiVersion.ts +++ b/src/apiVersion.ts @@ -1,3 +1,4 @@ // File generated from our OpenAPI spec export const ApiVersion = '2024-06-20'; +export const PreviewVersion = '2024-09-30.acacia'; diff --git a/src/autoPagination.ts b/src/autoPagination.ts index 19cb4a8984..24dd990c35 100644 --- a/src/autoPagination.ts +++ b/src/autoPagination.ts @@ -1,5 +1,9 @@ import {MethodSpec, RequestArgs, StripeResourceObject} from './Types.js'; -import {callbackifyPromiseWithTimeout, getDataFromArgs} from './utils.js'; +import { + callbackifyPromiseWithTimeout, + getDataFromArgs, + getAPIMode, +} from './utils.js'; type PromiseCache = { currentPromise: Promise | undefined | null; @@ -31,9 +35,10 @@ type AutoPaginationMethods = { type PageResult = { data: Array; has_more: boolean; - next_page: string | null; + next_page?: string | null; + next_page_url?: string | null; }; -class StripeIterator implements AsyncIterator { +class V1Iterator implements AsyncIterator { private index: number; private pagePromise: Promise>; private promiseCache: PromiseCache; @@ -117,7 +122,7 @@ class StripeIterator implements AsyncIterator { } } -class ListIterator extends StripeIterator { +class V1ListIterator extends V1Iterator { getNextPage(pageResult: PageResult): Promise> { const reverseIteration = isReverseIteration(this.requestArgs); const lastId = getLastId(pageResult, reverseIteration); @@ -127,7 +132,7 @@ class ListIterator extends StripeIterator { } } -class SearchIterator extends StripeIterator { +class V1SearchIterator extends V1Iterator { getNextPage(pageResult: PageResult): Promise> { if (!pageResult.next_page) { throw Error( @@ -140,6 +145,60 @@ class SearchIterator extends StripeIterator { } } +class V2ListIterator implements AsyncIterator { + private currentPageIterator: Promise>; + private nextPageUrl: Promise; + private requestArgs: RequestArgs; + private spec: MethodSpec; + private stripeResource: StripeResourceObject; + constructor( + firstPagePromise: Promise>, + requestArgs: RequestArgs, + spec: MethodSpec, + stripeResource: StripeResourceObject + ) { + this.currentPageIterator = (async (): Promise> => { + const page = await firstPagePromise; + return page.data[Symbol.iterator](); + })(); + + this.nextPageUrl = (async (): Promise => { + const page = await firstPagePromise; + return page.next_page_url || null; + })(); + + this.requestArgs = requestArgs; + this.spec = spec; + this.stripeResource = stripeResource; + } + private async turnPage(): Promise | null> { + const nextPageUrl = await this.nextPageUrl; + if (!nextPageUrl) return null; + this.spec.fullPath = nextPageUrl; + const page = await this.stripeResource._makeRequest( + this.requestArgs, + this.spec, + {} + ); + this.nextPageUrl = Promise.resolve(page.next_page_url); + this.currentPageIterator = Promise.resolve(page.data[Symbol.iterator]()); + return this.currentPageIterator; + } + async next(): Promise> { + { + const result = (await this.currentPageIterator).next(); + if (!result.done) return {done: false, value: result.value}; + } + const nextPageIterator = await this.turnPage(); + if (!nextPageIterator) { + return {done: true, value: undefined}; + } + const result = nextPageIterator.next(); + if (!result.done) return {done: false, value: result.value}; + return {done: true, value: undefined}; + } +} + export const makeAutoPaginationMethods = < TMethodSpec extends MethodSpec, TItem extends {id: string} @@ -149,14 +208,20 @@ export const makeAutoPaginationMethods = < spec: TMethodSpec, firstPagePromise: Promise> ): AutoPaginationMethods | null => { - if (spec.methodType === 'search') { + const apiMode = getAPIMode(spec.fullPath || spec.path); + if (apiMode !== 'v2' && spec.methodType === 'search') { + return makeAutoPaginationMethodsFromIterator( + new V1SearchIterator(firstPagePromise, requestArgs, spec, stripeResource) + ); + } + if (apiMode !== 'v2' && spec.methodType === 'list') { return makeAutoPaginationMethodsFromIterator( - new SearchIterator(firstPagePromise, requestArgs, spec, stripeResource) + new V1ListIterator(firstPagePromise, requestArgs, spec, stripeResource) ); } - if (spec.methodType === 'list') { + if (apiMode === 'v2' && spec.methodType === 'list') { return makeAutoPaginationMethodsFromIterator( - new ListIterator(firstPagePromise, requestArgs, spec, stripeResource) + new V2ListIterator(firstPagePromise, requestArgs, spec, stripeResource) ); } return null; diff --git a/src/crypto/CryptoProvider.ts b/src/crypto/CryptoProvider.ts index f362061c42..5d11947bb8 100644 --- a/src/crypto/CryptoProvider.ts +++ b/src/crypto/CryptoProvider.ts @@ -29,6 +29,13 @@ export class CryptoProvider { computeHMACSignatureAsync(payload: string, secret: string): Promise { throw new Error('computeHMACSignatureAsync not implemented.'); } + + /** + * Computes a SHA-256 hash of the data. + */ + computeSHA256Async(data: Uint8Array): Promise { + throw new Error('computeSHA256 not implemented.'); + } } /** diff --git a/src/crypto/NodeCryptoProvider.ts b/src/crypto/NodeCryptoProvider.ts index 477ce41039..fa86682151 100644 --- a/src/crypto/NodeCryptoProvider.ts +++ b/src/crypto/NodeCryptoProvider.ts @@ -21,4 +21,14 @@ export class NodeCryptoProvider extends CryptoProvider { const signature = await this.computeHMACSignature(payload, secret); return signature; } + + /** @override */ + async computeSHA256Async(data: Uint8Array): Promise { + return new Uint8Array( + await crypto + .createHash('sha256') + .update(data) + .digest() + ); + } } diff --git a/src/crypto/SubtleCryptoProvider.ts b/src/crypto/SubtleCryptoProvider.ts index 51ef0e9caa..db3b51a2c8 100644 --- a/src/crypto/SubtleCryptoProvider.ts +++ b/src/crypto/SubtleCryptoProvider.ts @@ -63,6 +63,11 @@ export class SubtleCryptoProvider extends CryptoProvider { return signatureHexCodes.join(''); } + + /** @override */ + async computeSHA256Async(data: Uint8Array): Promise { + return new Uint8Array(await this.subtleCrypto.digest('SHA-256', data)); + } } // Cached mapping of byte to hex representation. We do this once to avoid re- diff --git a/src/multipart.ts b/src/multipart.ts index 1e28eb44f9..85b6ce024b 100644 --- a/src/multipart.ts +++ b/src/multipart.ts @@ -4,7 +4,7 @@ import { RequestHeaders, StripeResourceObject, } from './Types.js'; -import {flattenAndStringify, stringifyRequestData} from './utils.js'; +import {flattenAndStringify, queryStringifyRequestData} from './utils.js'; type MultipartCallbackReturn = any; type MultipartCallback = ( @@ -87,7 +87,7 @@ export function multipartRequestDataProcessor( data = data || {}; if (method !== 'POST') { - return callback(null, stringifyRequestData(data)); + return callback(null, queryStringifyRequestData(data)); } this._stripe._platformFunctions diff --git a/src/net/FetchHttpClient.ts b/src/net/FetchHttpClient.ts index bea479b463..df5b3cf6b4 100644 --- a/src/net/FetchHttpClient.ts +++ b/src/net/FetchHttpClient.ts @@ -115,7 +115,7 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface { path: string, method: string, headers: RequestHeaders, - requestData: RequestData, + requestData: string, protocol: string, timeout: number ): Promise { diff --git a/src/net/HttpClient.ts b/src/net/HttpClient.ts index 3e886b6358..673be72a2e 100644 --- a/src/net/HttpClient.ts +++ b/src/net/HttpClient.ts @@ -10,7 +10,7 @@ export interface HttpClientInterface { path: string, method: string, headers: RequestHeaders, - requestData: RequestData, + requestData: string, protocol: string, timeout: number ) => Promise; @@ -48,7 +48,7 @@ export class HttpClient implements HttpClientInterface { path: string, method: string, headers: RequestHeaders, - requestData: RequestData, + requestData: string, protocol: string, timeout: number ): Promise { diff --git a/src/net/NodeHttpClient.ts b/src/net/NodeHttpClient.ts index e6f94f3a4b..562051582b 100644 --- a/src/net/NodeHttpClient.ts +++ b/src/net/NodeHttpClient.ts @@ -43,7 +43,7 @@ export class NodeHttpClient extends HttpClient { path: string, method: string, headers: RequestHeaders, - requestData: RequestData, + requestData: string, protocol: string, timeout: number ): Promise { diff --git a/src/resources.ts b/src/resources.ts index 109e6e9eb8..0643e9879e 100644 --- a/src/resources.ts +++ b/src/resources.ts @@ -118,6 +118,7 @@ export {TaxRates} from './resources/TaxRates.js'; export {Tokens} from './resources/Tokens.js'; export {Topups} from './resources/Topups.js'; export {Transfers} from './resources/Transfers.js'; +export {V2} from './resources/V2.js'; export {WebhookEndpoints} from './resources/WebhookEndpoints.js'; export const Apps = resourceNamespace('apps', {Secrets: AppsSecrets}); export const Billing = resourceNamespace('billing', { diff --git a/src/resources/OAuth.ts b/src/resources/OAuth.ts index 318d49835c..78f046a992 100644 --- a/src/resources/OAuth.ts +++ b/src/resources/OAuth.ts @@ -1,7 +1,7 @@ 'use strict'; import {StripeResource} from '../StripeResource.js'; -import {stringifyRequestData} from '../utils.js'; +import {queryStringifyRequestData} from '../utils.js'; type OAuthAuthorizeUrlParams = { response_type?: 'code'; @@ -48,7 +48,7 @@ export const OAuth = StripeResource.extend({ params.scope = 'read_write'; } - return `https://${oAuthHost}/${path}?${stringifyRequestData(params)}`; + return `https://${oAuthHost}/${path}?${queryStringifyRequestData(params)}`; }, token: stripeMethod({ diff --git a/src/resources/V2.ts b/src/resources/V2.ts new file mode 100644 index 0000000000..c8ad50c2e0 --- /dev/null +++ b/src/resources/V2.ts @@ -0,0 +1,12 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../StripeResource.js'; +import {Billing} from './V2/Billing.js'; +import {Core} from './V2/Core.js'; +export const V2 = StripeResource.extend({ + constructor: function(...args: any) { + StripeResource.apply(this, args); + this.billing = new Billing(...args); + this.core = new Core(...args); + }, +}); diff --git a/src/resources/V2/Billing.ts b/src/resources/V2/Billing.ts new file mode 100644 index 0000000000..752a0d3a10 --- /dev/null +++ b/src/resources/V2/Billing.ts @@ -0,0 +1,16 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../StripeResource.js'; +import {MeterEventSession} from './Billing/MeterEventSession.js'; +import {MeterEventAdjustments} from './Billing/MeterEventAdjustments.js'; +import {MeterEventStream} from './Billing/MeterEventStream.js'; +import {MeterEvents} from './Billing/MeterEvents.js'; +export const Billing = StripeResource.extend({ + constructor: function(...args: any) { + StripeResource.apply(this, args); + this.meterEventSession = new MeterEventSession(...args); + this.meterEventAdjustments = new MeterEventAdjustments(...args); + this.meterEventStream = new MeterEventStream(...args); + this.meterEvents = new MeterEvents(...args); + }, +}); diff --git a/src/resources/V2/Billing/MeterEventAdjustments.ts b/src/resources/V2/Billing/MeterEventAdjustments.ts new file mode 100644 index 0000000000..e6bc084da5 --- /dev/null +++ b/src/resources/V2/Billing/MeterEventAdjustments.ts @@ -0,0 +1,10 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../../StripeResource.js'; +const stripeMethod = StripeResource.method; +export const MeterEventAdjustments = StripeResource.extend({ + create: stripeMethod({ + method: 'POST', + fullPath: '/v2/billing/meter_event_adjustments', + }), +}); diff --git a/src/resources/V2/Billing/MeterEventSession.ts b/src/resources/V2/Billing/MeterEventSession.ts new file mode 100644 index 0000000000..2fa6fb9f8e --- /dev/null +++ b/src/resources/V2/Billing/MeterEventSession.ts @@ -0,0 +1,10 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../../StripeResource.js'; +const stripeMethod = StripeResource.method; +export const MeterEventSession = StripeResource.extend({ + create: stripeMethod({ + method: 'POST', + fullPath: '/v2/billing/meter_event_session', + }), +}); diff --git a/src/resources/V2/Billing/MeterEventStream.ts b/src/resources/V2/Billing/MeterEventStream.ts new file mode 100644 index 0000000000..c74537f037 --- /dev/null +++ b/src/resources/V2/Billing/MeterEventStream.ts @@ -0,0 +1,11 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../../StripeResource.js'; +const stripeMethod = StripeResource.method; +export const MeterEventStream = StripeResource.extend({ + create: stripeMethod({ + method: 'POST', + fullPath: '/v2/billing/meter_event_stream', + host: 'meter-events.stripe.com', + }), +}); diff --git a/src/resources/V2/Billing/MeterEvents.ts b/src/resources/V2/Billing/MeterEvents.ts new file mode 100644 index 0000000000..ae425b29b5 --- /dev/null +++ b/src/resources/V2/Billing/MeterEvents.ts @@ -0,0 +1,7 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../../StripeResource.js'; +const stripeMethod = StripeResource.method; +export const MeterEvents = StripeResource.extend({ + create: stripeMethod({method: 'POST', fullPath: '/v2/billing/meter_events'}), +}); diff --git a/src/resources/V2/Core.ts b/src/resources/V2/Core.ts new file mode 100644 index 0000000000..4f13f0cc90 --- /dev/null +++ b/src/resources/V2/Core.ts @@ -0,0 +1,10 @@ +// File generated from our OpenAPI spec + +import {StripeResource} from '../../StripeResource.js'; +import {Events} from './Core/Events.js'; +export const Core = StripeResource.extend({ + constructor: function(...args: any) { + StripeResource.apply(this, args); + this.events = new Events(...args); + }, +}); diff --git a/src/resources/V2/Core/Events.ts b/src/resources/V2/Core/Events.ts new file mode 100644 index 0000000000..1bde76d2ee --- /dev/null +++ b/src/resources/V2/Core/Events.ts @@ -0,0 +1,50 @@ +// This file is manually maintained + +import {StripeResource} from '../../../StripeResource.js'; + +const stripeMethod = StripeResource.method; +const addFetchObjectToPulledEvent = (pulledEvent: { + related_object: any; + context?: string; +}): any => { + if (!pulledEvent.related_object || !pulledEvent.related_object.url) { + return pulledEvent; + } + return { + ...pulledEvent, + fetchRelatedObject: (): Promise => { + return stripeMethod({ + method: 'GET', + fullPath: pulledEvent.related_object.url, + })({ + stripeAccount: pulledEvent.context, + }); + }, + }; +}; + +export const Events = StripeResource.extend({ + retrieve(...args: any[]) { + return stripeMethod({ + method: 'GET', + fullPath: '/v2/core/events/{id}', + }) + .apply(this, args) + .then((pulledEvent) => { + return addFetchObjectToPulledEvent(pulledEvent); + }); + }, + list(...args: any[]) { + stripeMethod({ + method: 'GET', + fullPath: '/v2/core/events', + methodType: 'list', + }) + .apply(this, args) + .then((pulledEvents) => { + return pulledEvents.map((pulledEvent: {related_object: {url: any}}) => { + return addFetchObjectToPulledEvent(pulledEvent); + }); + }); + }, +}); diff --git a/src/stripe.core.ts b/src/stripe.core.ts index b777ce7cdb..d0e59b14df 100644 --- a/src/stripe.core.ts +++ b/src/stripe.core.ts @@ -1,14 +1,22 @@ import * as _Error from './Error.js'; import {RequestSender} from './RequestSender.js'; import {StripeResource} from './StripeResource.js'; -import {AppInfo, StripeObject, UserProvidedConfig} from './Types.js'; -import {WebhookObject, createWebhooks} from './Webhooks.js'; -import * as apiVersion from './apiVersion.js'; +import { + AppInfo, + RequestAuthenticator, + RequestHeaders, + StripeObject, + StripeRequest, + UserProvidedConfig, +} from './Types.js'; +import {WebhookObject, WebhookEvent, createWebhooks} from './Webhooks.js'; +import {ApiVersion} from './apiVersion.js'; import {CryptoProvider} from './crypto/CryptoProvider.js'; import {HttpClient, HttpClientResponse} from './net/HttpClient.js'; import {PlatformFunctions} from './platform/PlatformFunctions.js'; import * as resources from './resources.js'; import { + createApiKeyAuthenticator, determineProcessUserAgentProperties, pascalToCamelCase, validateInteger, @@ -17,15 +25,16 @@ import { const DEFAULT_HOST = 'api.stripe.com'; const DEFAULT_PORT = '443'; const DEFAULT_BASE_PATH = '/v1/'; -const DEFAULT_API_VERSION = apiVersion.ApiVersion; +const DEFAULT_API_VERSION = ApiVersion; const DEFAULT_TIMEOUT = 80000; -const MAX_NETWORK_RETRY_DELAY_SEC = 2; +const MAX_NETWORK_RETRY_DELAY_SEC = 5; const INITIAL_NETWORK_RETRY_DELAY_SEC = 0.5; const APP_INFO_PROPERTIES = ['name', 'version', 'url', 'partner_id']; const ALLOWED_CONFIG_PROPERTIES = [ + 'authenticator', 'apiVersion', 'typescript', 'maxNetworkRetries', @@ -38,6 +47,7 @@ const ALLOWED_CONFIG_PROPERTIES = [ 'telemetry', 'appInfo', 'stripeAccount', + 'stripeContext', ]; type RequestSenderFactory = (stripe: StripeObject) => RequestSender; @@ -49,7 +59,7 @@ export function createStripe( platformFunctions: PlatformFunctions, requestSender: RequestSenderFactory = defaultRequestSenderFactory ): typeof Stripe { - Stripe.PACKAGE_VERSION = '16.12.0'; + Stripe.PACKAGE_VERSION = '0.56.0'; Stripe.USER_AGENT = { bindings_version: Stripe.PACKAGE_VERSION, lang: 'node', @@ -107,7 +117,6 @@ export function createStripe( const agent = props.httpAgent || null; this._api = { - auth: null, host: props.host || DEFAULT_HOST, port: props.port || DEFAULT_PORT, protocol: props.protocol || 'https', @@ -117,7 +126,7 @@ export function createStripe( maxNetworkRetries: validateInteger( 'maxNetworkRetries', props.maxNetworkRetries, - 1 + 2 ), agent: agent, httpClient: @@ -127,6 +136,7 @@ export function createStripe( : this._platformFunctions.createDefaultHttpClient()), dev: false, stripeAccount: props.stripeAccount || null, + stripeContext: props.stripeContext || null, }; const typescript = props.typescript || false; @@ -143,7 +153,7 @@ export function createStripe( } this._prepResources(); - this._setApiKey(key); + this._setAuthenticator(key, props.authenticator); this.errors = _Error; @@ -191,6 +201,110 @@ export function createStripe( Stripe.createSubtleCryptoProvider = platformFunctions.createSubtleCryptoProvider; + Stripe.createRequestSigningAuthenticator = ( + keyId: string, + sign: (signatureBase: Uint8Array) => Promise, + crypto: CryptoProvider = Stripe.createNodeCryptoProvider() + ): RequestAuthenticator => { + const authorizationHeaderName = 'Authorization'; + const stripeContextHeaderName = 'Stripe-Context'; + const stripeAccountHeaderName = 'Stripe-Account'; + const contentDigestHeaderName = 'Content-Digest'; + const signatureInputHeaderName = 'Signature-Input'; + const signatureHeaderName = 'Signature'; + const coveredHeaderNames = [ + 'Content-Type', + contentDigestHeaderName, + stripeContextHeaderName, + stripeAccountHeaderName, + authorizationHeaderName, + ].map((h) => h.toLowerCase()); + + const coveredHeaderNamesGet = [ + stripeContextHeaderName, + stripeAccountHeaderName, + authorizationHeaderName, + ].map((h) => h.toLowerCase()); + + const formatCoveredHeaders = (headers: Array): string => { + return `(${headers.map((h) => `"${h}"`).join(' ')})`; + }; + const coveredHeaderFormatted = formatCoveredHeaders(coveredHeaderNames); + const coveredHeaderGetFormatted = formatCoveredHeaders( + coveredHeaderNamesGet + ); + + const getSignatureInput = (method: string, created: number): string => { + const coveredHeaderNames = + method == 'GET' ? coveredHeaderGetFormatted : coveredHeaderFormatted; + return `${coveredHeaderNames};created=${created}`; + }; + + const encoder = new TextEncoder(); + + const initializedCrypto = crypto ?? Stripe.createNodeCryptoProvider(); + + const calculateDigestHeader = async (content: string): Promise => { + const digest = await initializedCrypto.computeSHA256Async( + encoder.encode(content) + ); + return `sha-256=:${Buffer.from(digest).toString('base64')}:`; + }; + + const calculateSignatureBase = ( + request: StripeRequest, + created: number + ): string => { + const stringifyHeaderValues = ( + value: string | number | string[] | null + ): string => { + if (value == null) { + return ''; + } + return (value instanceof Array ? value : [value]).join(','); + }; + + const headerNames = + request.method == 'GET' ? coveredHeaderNamesGet : coveredHeaderNames; + const lowercaseHeaders: RequestHeaders = {}; + const keys = Object.keys(request.headers); + keys.forEach((k) => { + lowercaseHeaders[k.toLowerCase()] = request.headers[k]; + }); + + const inputs = headerNames + .map( + (header) => + `"${header}": ${stringifyHeaderValues(lowercaseHeaders[header])}` + ) + .join('\n'); + + const signatureInput = getSignatureInput(request.method, created); + + return `${inputs}\n"@signature-params": ${signatureInput}`; + }; + + return async (request): Promise => { + if (request.method != 'GET') { + request.headers[contentDigestHeaderName] = await calculateDigestHeader( + request.body ?? '' + ); + } + + const created = Math.floor(Date.now() / 1000); + + request.headers[authorizationHeaderName] = 'STRIPE-V2-SIG ' + keyId; + request.headers[signatureInputHeaderName] = + 'sig1=' + getSignatureInput(request.method, created); + + const signatureBase = calculateSignatureBase(request, created); + const signature = await sign(encoder.encode(signatureBase)); + + request.headers[signatureHeaderName] = + 'sig1=:' + Buffer.from(signature).toString('base64') + ':'; + }; + }; + Stripe.prototype = { // Properties are set in the constructor above _appInfo: undefined!, @@ -211,10 +325,21 @@ export function createStripe( /** * @private */ - _setApiKey(key: string): void { - if (key) { - this._setApiField('auth', `Bearer ${key}`); + _setAuthenticator( + key: string, + authenticator: RequestAuthenticator | undefined + ): void { + if (key && authenticator) { + throw new Error("Can't specify both apiKey and authenticator"); } + + if (!key && !authenticator) { + throw new Error('Neither apiKey nor config.authenticator provided'); + } + + this._authenticator = key + ? createApiKeyAuthenticator(key) + : authenticator; }, /** @@ -469,6 +594,43 @@ export function createStripe( return config; }, + + parseThinEvent( + payload, + header, + secret, + tolerance, + cryptoProvider, + receivedAt + ): WebhookEvent { + // parses and validates the event payload all in one go + return this.webhooks.constructEvent( + payload, + header, + secret, + tolerance, + cryptoProvider, + receivedAt + ); + }, + + parseSnapshotEvent( + payload, + header, + secret, + tolerance, + cryptoProvider, + receivedAt + ): WebhookEvent { + return this.webhooks.constructEvent( + payload, + header, + secret, + tolerance, + cryptoProvider, + receivedAt + ); + }, } as StripeObject; return Stripe; diff --git a/src/utils.ts b/src/utils.ts index 005256d56e..4ad04bf64e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,9 @@ import { StripeResourceObject, RequestHeaders, MultipartRequestData, + RequestAuthenticator, + StripeRequest, + ApiMode, } from './Types.js'; const OPTIONS_KEYS = [ @@ -16,6 +19,8 @@ const OPTIONS_KEYS = [ 'maxNetworkRetries', 'timeout', 'host', + 'authenticator', + 'stripeContext', ]; type Settings = { @@ -24,7 +29,7 @@ type Settings = { }; type Options = { - auth: string | null; + authenticator?: RequestAuthenticator | null; host: string | null; settings: Settings; streaming?: boolean; @@ -43,11 +48,15 @@ export function isOptionsHash(o: unknown): boolean | unknown { * Stringifies an Object, accommodating nested objects * (forming the conventional key 'parent[child]=value') */ -export function stringifyRequestData(data: RequestData | string): string { +export function queryStringifyRequestData( + data: RequestData | string, + apiMode?: ApiMode +): string { return ( qs .stringify(data, { serializeDate: (d: Date) => Math.floor(d.getTime() / 1000).toString(), + arrayFormat: apiMode == 'v2' ? 'repeat' : 'indices', }) // Don't use strict form encoding by changing the square bracket control // characters back to their literals. This is fine by the server, and @@ -132,7 +141,6 @@ export function getDataFromArgs(args: RequestArgs): RequestData { */ export function getOptionsFromArgs(args: RequestArgs): Options { const opts: Options = { - auth: null, host: null, headers: {}, settings: {}, @@ -140,7 +148,7 @@ export function getOptionsFromArgs(args: RequestArgs): Options { if (args.length > 0) { const arg = args[args.length - 1]; if (typeof arg === 'string') { - opts.auth = args.pop() as string; + opts.authenticator = createApiKeyAuthenticator(args.pop() as string); } else if (isOptionsHash(arg)) { const params = {...(args.pop() as Record)}; @@ -155,7 +163,7 @@ export function getOptionsFromArgs(args: RequestArgs): Options { } if (params.apiKey) { - opts.auth = params.apiKey as string; + opts.authenticator = createApiKeyAuthenticator(params.apiKey as string); } if (params.idempotencyKey) { opts.headers['Idempotency-Key'] = params.idempotencyKey; @@ -163,6 +171,14 @@ export function getOptionsFromArgs(args: RequestArgs): Options { if (params.stripeAccount) { opts.headers['Stripe-Account'] = params.stripeAccount; } + if (params.stripeContext) { + if (opts.headers['Stripe-Account']) { + throw new Error( + "Can't specify both stripeAccount and stripeContext." + ); + } + opts.headers['Stripe-Context'] = params.stripeContext; + } if (params.apiVersion) { opts.headers['Stripe-Version'] = params.apiVersion; } @@ -175,6 +191,18 @@ export function getOptionsFromArgs(args: RequestArgs): Options { if (params.host) { opts.host = params.host as string; } + if (params.authenticator) { + if (params.apiKey) { + throw new Error("Can't specify both apiKey and authenticator."); + } + if (typeof params.authenticator !== 'function') { + throw new Error( + 'The authenticator must be a function ' + + 'receiving a request as the first parameter.' + ); + } + opts.authenticator = params.authenticator as RequestAuthenticator; + } } } return opts; @@ -361,6 +389,20 @@ export function determineProcessUserAgentProperties(): Record { }; } +export function createApiKeyAuthenticator( + apiKey: string +): RequestAuthenticator { + const authenticator = (request: StripeRequest): Promise => { + request.headers.Authorization = 'Bearer ' + apiKey; + return Promise.resolve(); + }; + + // For testing + authenticator._apiKey = apiKey; + + return authenticator; +} + /** * Joins an array of Uint8Arrays into a single Uint8Array */ @@ -376,3 +418,31 @@ export function concat(arrays: Array): Uint8Array { return merged; } + +/** + * Replaces Date objects with Unix timestamps + */ +function dateTimeReplacer(this: any, key: string, value: any): string { + if (this[key] instanceof Date) { + return Math.floor(this[key].getTime() / 1000).toString(); + } + + return value; +} + +/** + * JSON stringifies an Object, replacing Date objects with Unix timestamps + */ +export function jsonStringifyRequestData(data: RequestData | string): string { + return JSON.stringify(data, dateTimeReplacer); +} + +/** + * Inspects the given path to determine if the endpoint is for v1 or v2 API + */ +export function getAPIMode(path?: string): ApiMode { + if (!path) { + return 'v1'; + } + return path.startsWith('/v2') ? 'v2' : 'v1'; +} diff --git a/test/Error.spec.ts b/test/Error.spec.ts index a899766a7a..2085209338 100644 --- a/test/Error.spec.ts +++ b/test/Error.spec.ts @@ -23,6 +23,30 @@ describe('Error', () => { ).to.be.instanceOf(Error.StripeUnknownError); }); + it('Generates specific instance of v2 errors depending on error-type', () => { + // Falls back to V1 parsing logic if code is absent + expect(Error.generateV2({type: 'card_error'})).to.be.instanceOf( + Error.StripeCardError + ); + // Falls back to V1 parsing logic if code is unrecognized + expect( + Error.generateV2({type: 'card_error', code: 'no_such_error'}) + ).to.be.instanceOf(Error.StripeCardError); + expect( + Error.generateV2({ + type: 'insufficient_funds', + code: 'outbound_payment_insufficient_funds', + }) + ).to.be.instanceOf(Error.InsufficientFundsError); + expect(Error.generateV2({type: 'blocked_by_stripe'})).to.be.instanceOf( + Error.BlockedByStripeError + ); + + expect(Error.generateV2({code: 'invalid_fields'})).to.be.instanceOf( + Error.StripeInvalidRequestError + ); + }); + it('copies whitelisted properties', () => { const e = new Error.StripeError({ charge: 'foo', diff --git a/test/RequestSender.spec.ts b/test/RequestSender.spec.ts index 9ccf7b1ea3..b6e4718891 100644 --- a/test/RequestSender.spec.ts +++ b/test/RequestSender.spec.ts @@ -1,21 +1,25 @@ // @ts-nocheck import {expect} from 'chai'; +import nock = require('nock'); + import { + InsufficientFundsError, StripeAuthenticationError, StripeConnectionError, StripeError, StripeIdempotencyError, StripePermissionError, StripeRateLimitError, + StripeUnknownError, } from '../src/Error.js'; -import {HttpClientResponse} from '../src/net/HttpClient.js'; import {RequestSender} from '../src/RequestSender.js'; +import {ApiVersion, PreviewVersion} from '../src/apiVersion.js'; +import {HttpClientResponse} from '../src/net/HttpClient.js'; import { FAKE_API_KEY, getSpyableStripe, getTestServerStripe, } from './testUtils.js'; -import nock = require('nock'); const stripe = getSpyableStripe(); @@ -23,22 +27,46 @@ describe('RequestSender', () => { const sender = new RequestSender(stripe, 0); describe('_makeHeaders', () => { - it('sets the Authorization header with Bearer auth using the global API key', () => { - const headers = sender._makeHeaders(null, 0, null); - expect(headers.Authorization).to.equal(`Bearer ${FAKE_API_KEY}`); - }); - it('sets the Authorization header with Bearer auth using the specified API key', () => { - const headers = sender._makeHeaders('anotherFakeAuthToken', 0, null); - expect(headers.Authorization).to.equal('Bearer anotherFakeAuthToken'); - }); it('sets the Stripe-Version header if an API version is provided', () => { - const headers = sender._makeHeaders(null, 0, '1970-01-01'); + const headers = sender._makeHeaders({apiVersion: '1970-01-01'}); expect(headers['Stripe-Version']).to.equal('1970-01-01'); }); it('does not the set the Stripe-Version header if no API version is provided', () => { - const headers = sender._makeHeaders(null, 0, null); + const headers = sender._makeHeaders({}); expect(headers).to.not.include.keys('Stripe-Version'); }); + describe('idempotency keys', () => { + it('only creates creates an idempotency key if a v1 request wil retry', () => { + const headers = sender._makeHeaders({ + method: 'POST', + userSuppliedSettings: {maxNetworkRetries: 3}, + apiMode: 'v1', + }); + expect(headers['Idempotency-Key']).matches(/^stripe-node-retry/); + }); + // should probably always create an IK; until then, codify the behavior + it("skips idempotency genration for v1 reqeust if we're not retrying the request", () => { + const headers = sender._makeHeaders({ + method: 'POST', + userSuppliedSettings: {maxNetworkRetries: 0}, + apiMode: 'v1', + }); + expect(headers['Idempotency-Key']).equals(undefined); + }); + it('always creates an idempotency key for v2 POST requests', () => { + const headers = sender._makeHeaders({method: 'POST', apiMode: 'v2'}); + expect(headers['Idempotency-Key']).matches(/^stripe-node-retry/); + }); + it('always creates an idempotency key for v2 DELETE requests', () => { + const headers = sender._makeHeaders({method: 'DELETE', apiMode: 'v2'}); + expect(headers['Idempotency-Key']).matches(/^stripe-node-retry/); + }); + it('generates a new key every time', () => { + expect(sender._defaultIdempotencyKey('POST', {}, 'v2')).not.to.equal( + sender._defaultIdempotencyKey('POST', {}, 'v2') + ); + }); + }); }); describe('_shouldRetry', () => { @@ -90,7 +118,7 @@ describe('RequestSender', () => { describe('Parameter encoding', () => { // Use a real instance of stripe as we're mocking the http.request responses. - const realStripe = require('../src/stripe.cjs.node.js')('sk_test_xyz'); + const realStripe = require('../src/stripe.cjs.node.js')(FAKE_API_KEY); afterEach(() => { nock.cleanAll(); @@ -268,9 +296,12 @@ describe('RequestSender', () => { const scope = nock( `https://${options.host}`, - // Content-Length should be present for POST. + // Content-Length and Content-Type should be present for POST. { - reqheaders: {'Content-Length': options.body.length}, + reqheaders: { + 'Content-Length': options.body.length, + 'Content-Type': 'application/x-www-form-urlencoded', + }, } ) .post(options.path, options.body) @@ -286,6 +317,83 @@ describe('RequestSender', () => { ); }); + it('encodes the body in POST requests as JSON for v2', (done) => { + const options = { + host: stripe.getConstant('DEFAULT_HOST'), + path: '/v2/accounts', + data: { + name: 'llama', + }, + body: '{"name":"llama"}', + }; + + const scope = nock( + `https://${options.host}`, + // Content-Length and Content-Type should be present for POST. + { + reqheaders: { + 'Content-Length': options.body.length, + 'Content-Type': 'application/json', + }, + } + ) + .post(options.path, options.body) + .reply(200, '{}'); + + realStripe.v2.accounts.create(options.data, (err, response) => { + done(err); + scope.done(); + }); + }); + + it('encodes null values in the body in POST correctly for v2', (done) => { + const options = { + host: stripe.getConstant('DEFAULT_HOST'), + path: '/v2/accounts', + data: { + name: null, + }, + body: '{"name":null}', + }; + + const scope = nock( + `https://${options.host}`, + // Content-Length and Content-Type should be present for POST. + { + reqheaders: { + 'Content-Length': options.body.length, + 'Content-Type': 'application/json', + }, + } + ) + .post(options.path, options.body) + .reply(200, '{}'); + + realStripe.v2.accounts.create(options.data, (err, response) => { + done(err); + scope.done(); + }); + }); + + it('encodes data for GET requests as query params for v2', (done) => { + const host = stripe.getConstant('DEFAULT_HOST'); + const scope = nock(`https://${host}`) + .get( + `/v2/accounts/acc_123?include=defaults&include=configuration`, + '' + ) + .reply(200, '{}'); + + realStripe.v2.accounts.retrieve( + 'acc_123', + {include: ['defaults', 'configuration']}, + (err, response) => { + done(err); + scope.done(); + } + ); + }); + it('always includes Content-Length in POST requests even when empty', (done) => { const options = { host: stripe.getConstant('DEFAULT_HOST'), @@ -315,6 +423,35 @@ describe('RequestSender', () => { ); }); + it('encodes Date objects in POST requests as JSON for v2', (done) => { + const options = { + host: stripe.getConstant('DEFAULT_HOST'), + path: '/v2/accounts', + data: { + created: new Date('2009-02-13T23:31:30Z'), + }, + body: '{"created":"1234567890"}', + }; + + const scope = nock( + `https://${options.host}`, + // Content-Length and Content-Type should be present for POST. + { + reqheaders: { + 'Content-Length': options.body.length, + 'Content-Type': 'application/json', + }, + } + ) + .post(options.path, options.body) + .reply(200, '{}'); + + realStripe.v2.accounts.create(options.data, (err, response) => { + done(err); + scope.done(); + }); + }); + it('allows overriding host', (done) => { const scope = nock('https://myhost') .get('/v1/accounts/acct_123') @@ -332,6 +469,33 @@ describe('RequestSender', () => { } ); }); + + it('sends current v1 version when apiMode is v1', (done) => { + const host = stripe.getConstant('DEFAULT_HOST'); + const scope = nock(`https://${host}`, { + reqheaders: {'Stripe-Version': ApiVersion}, + }) + .get('/v1/subscriptions') + .reply(200, '{}'); + + realStripe.subscriptions.list((err, response) => { + done(err); + scope.done(); + }); + }); + it('sends current v2 version when apiMode is v2', (done) => { + const host = stripe.getConstant('DEFAULT_HOST'); + const scope = nock(`https://${host}`, { + reqheaders: {'Stripe-Version': PreviewVersion}, + }) + .get('/v2/accounts') + .reply(200, '{}'); + + realStripe.v2.accounts.list((err, response) => { + done(err); + scope.done(); + }); + }); }); }); @@ -509,6 +673,47 @@ describe('RequestSender', () => { }); }); + it('throws a v2 StripeError based on the underlying error "code" if apiMode is v2', (done) => { + const error = { + type: 'insufficient_funds', + message: 'you messed up', + }; + + nock(`https://${options.host}`) + .post('/v2/accounts', {}) + .reply(400, { + error, + }); + + realStripe.v2.accounts.create({}, (err) => { + expect(err).to.be.an.instanceOf(InsufficientFundsError); + expect(err.message).to.equal('you messed up'); + done(); + }); + }); + + it('throws a v1 StripeError if apiMode is NOT v2', (done) => { + const error = { + type: 'insufficient_funds', + message: 'you messed up', + }; + + nock(`https://${options.host}`) + .post('/v1/customers', {}) + .reply(400, { + error, + }); + + realStripe.customers.create({}, (err) => { + expect(err).to.be.an.instanceOf(StripeError); + expect(err).to.be.an.instanceOf(StripeUnknownError); + expect(err).not.to.be.an.instanceOf(InsufficientFundsError); + expect(err.message).to.equal('you messed up'); + expect(err.raw.message).to.equal('you messed up'); + done(); + }); + }); + it('retries connection timeout errors', (done) => { let nRequestsReceived = 0; return getTestServerStripe( diff --git a/test/autoPagination.spec.ts b/test/autoPagination.spec.ts index 1c5f99db1e..063aad5211 100644 --- a/test/autoPagination.spec.ts +++ b/test/autoPagination.spec.ts @@ -5,6 +5,7 @@ import {expect} from 'chai'; import {makeAutoPaginationMethods} from '../src/autoPagination.js'; import {StripeResource} from '../src/StripeResource.js'; import {getMockStripe} from './testUtils.js'; +import {MethodSpec} from '../src/Types.js'; describe('auto pagination', () => { const testCase = (mockPaginationFn) => ({ @@ -36,7 +37,6 @@ describe('auto pagination', () => { method: 'GET', fullPath: '/v1/items', methodType: 'list', - apiMode: 'v1', }; const mockStripe = getMockStripe( @@ -544,7 +544,7 @@ describe('auto pagination', () => { }); }); - describe('foward pagination', () => { + describe('forward pagination', () => { it('paginates forwards through a page', () => { return testCaseV1List({ pages: [ @@ -647,7 +647,6 @@ describe('auto pagination', () => { const spec = { method: 'GET', methodType: 'search', - apiMode: 'v1', }; const addNextPage = (props) => { @@ -746,6 +745,103 @@ describe('auto pagination', () => { }); }); }); + describe('V2 list pagination', () => { + const mockPaginationV2List = (pages, initialArgs) => { + let i = 1; + const paramsLog = []; + const spec = { + method: 'GET', + fullPath: '/v2/items', + methodType: 'list', + }; + + const mockStripe = getMockStripe( + {}, + (_1, _2, path, _4, _5, _6, _7, callback) => { + paramsLog.push(path.slice(path.indexOf('?'))); + callback( + null, + Promise.resolve({ + data: pages[i].ids.map((id) => ({id})), + next_page_url: pages[i].next_page_url, + }) + ); + i += 1; + } + ); + const resource = new StripeResource(mockStripe); + + const paginator = makeAutoPaginationMethods( + resource, + initialArgs || {}, + spec, + Promise.resolve({ + data: pages[0].ids.map((id) => ({id})), + next_page_url: pages[0].next_page_url, + }) + ); + return {paginator, paramsLog}; + }; + + const testCaseV2List = testCase(mockPaginationV2List); + it('paginates forwards through a page', () => { + return testCaseV2List({ + pages: [ + {ids: [1, 2], next_page_url: '/v2/items?page=foo'}, + {ids: [3, 4]}, + ], + limit: 10, + expectedIds: [1, 2, 3, 4], + expectedParamsLog: ['?page=foo'], + }); + }); + + it('paginates forwards through uneven-sized pages', () => { + return testCaseV2List({ + pages: [ + {ids: [1, 2], next_page_url: '/v2/items?page=foo'}, + {ids: [3, 4], next_page_url: '/v2/items?page=bar'}, + {ids: [5]}, + ], + limit: 10, + expectedIds: [1, 2, 3, 4, 5], + expectedParamsLog: ['?page=foo', '?page=bar'], + }); + }); + + it('respects limit even when paginating', () => { + return testCaseV2List({ + pages: [ + {ids: [1, 2], next_page_url: '/v2/items?limit=5&page=a'}, + {ids: [3, 4], next_page_url: '/v2/items?limit=5&page=b'}, + {ids: [5, 6]}, + ], + limit: 5, + expectedIds: [1, 2, 3, 4, 5], + expectedParamsLog: ['?limit=5&page=a', '?limit=5&page=b'], + }); + }); + + it('paginates through multiple full pages', () => { + return testCaseV2List({ + pages: [ + {ids: [1, 2], next_page_url: '/v2/items?limit=10&page=wibble'}, + {ids: [3, 4], next_page_url: '/v2/items?limit=10&page=wobble'}, + {ids: [5, 6], next_page_url: '/v2/items?limit=10&page=weeble'}, + {ids: [7, 8], next_page_url: '/v2/items?limit=10&page=blubble'}, + {ids: [9, 10]}, + ], + limit: 10, + expectedIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + expectedParamsLog: [ + '?limit=10&page=wibble', + '?limit=10&page=wobble', + '?limit=10&page=weeble', + '?limit=10&page=blubble', + ], + }); + }); + }); }); export {}; diff --git a/test/crypto/helpers.ts b/test/crypto/helpers.ts index a056283c6c..3e5a905224 100644 --- a/test/crypto/helpers.ts +++ b/test/crypto/helpers.ts @@ -57,5 +57,16 @@ export const createCryptoProviderTestSuite = ( }); } }); + + describe('computeSHA256Async', () => { + it('computes the hash', async () => { + const signature = await cryptoProvider.computeSHA256Async( + new Uint8Array([1, 2, 3, 4, 5]) + ); + expect(Buffer.from(signature).toString('base64')).to.equal( + 'dPgf4WfZm0y0HW0MzagieMrunz4vJdXlo5Nv89zsYNA=' + ); + }); + }); }); }; diff --git a/test/net/helpers.ts b/test/net/helpers.ts index 6154b86473..cdec2b0e5f 100644 --- a/test/net/helpers.ts +++ b/test/net/helpers.ts @@ -101,7 +101,7 @@ export const createHttpClientTestSuite = (createHttpClientFn, extraTestsFn) => { }); it('sends request data (POST)', (done) => { - const expectedData = utils.stringifyRequestData({id: 'test'}); + const expectedData = utils.queryStringifyRequestData({id: 'test'}); nock('http://stripe.com') .post('/test') diff --git a/test/resources/generated_examples_test.spec.js b/test/resources/generated_examples_test.spec.js index 8a87f2869d..721ecc434d 100644 --- a/test/resources/generated_examples_test.spec.js +++ b/test/resources/generated_examples_test.spec.js @@ -567,6 +567,19 @@ describe('Generated tests', function() { expect(session).not.to.be.null; }); + it('test_core_events_get', async function() { + const stripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events/ll_123', + response: + '{"context":"context","created":"1970-01-12T21:42:34.472Z","id":"obj_123","livemode":true,"object":"event","reason":{"type":"request","request":{"id":"obj_123","idempotency_key":"idempotency_key"}},"type":"type"}', + }, + ]); + const event = await stripe.v2.core.events.retrieve('ll_123'); + expect(event).not.to.be.null; + }); + it('test_country_specs_get', async function() { const countrySpecs = await stripe.countrySpecs.list({ limit: 3, diff --git a/test/stripe.spec.ts b/test/stripe.spec.ts index 79d702bf92..836768c186 100644 --- a/test/stripe.spec.ts +++ b/test/stripe.spec.ts @@ -4,14 +4,16 @@ 'use strict'; import {expect} from 'chai'; +import {StripeSignatureVerificationError} from '../src/Error.js'; import {ApiVersion} from '../src/apiVersion.js'; import {createStripe} from '../src/stripe.core.js'; +import {createApiKeyAuthenticator} from '../src/utils.js'; import { + FAKE_API_KEY, getMockPlatformFunctions, getRandomString, - getTestServerStripe, getStripeMockClient, - FAKE_API_KEY, + getTestServerStripe, } from './testUtils.js'; import Stripe = require('../src/stripe.cjs.node.js'); import crypto = require('crypto'); @@ -65,7 +67,7 @@ describe('Stripe Module', function() { }).to.not.throw(); }); - it('should perform a no-op if null, undefined or empty values are passed', () => { + it('API should use the default version when undefined or empty values are passed', () => { const cases = [null, undefined, '', {}]; cases.forEach((item) => { @@ -108,7 +110,32 @@ describe('Stripe Module', function() { describe('setApiKey', () => { it('uses Bearer auth', () => { - expect(stripe.getApiField('auth')).to.equal(`Bearer ${FAKE_API_KEY}`); + expect(stripe._authenticator._apiKey).to.equal(`${FAKE_API_KEY}`); + }); + + it('should throw if no api key or authenticator provided', () => { + expect(() => new Stripe(null)).to.throw( + 'Neither apiKey nor config.authenticator provided' + ); + }); + }); + + describe('authenticator', () => { + it('should throw an error when specifying both key and authenticator', () => { + expect(() => { + return new Stripe('key', { + authenticator: createApiKeyAuthenticator('...'), + }); + }).to.throw("Can't specify both apiKey and authenticator"); + }); + + it('can create client using authenticator', () => { + const authenticator = createApiKeyAuthenticator('...'); + const stripe = new Stripe(null, { + authenticator: authenticator, + }); + + expect(stripe._authenticator).to.equal(authenticator); }); }); @@ -529,6 +556,117 @@ describe('Stripe Module', function() { ); }); }); + describe('gets removed', () => { + let headers; + let stripeClient; + let closeServer; + beforeEach((callback) => { + getTestServerStripe( + {}, + (req, res) => { + headers = req.headers; + res.writeHeader(200); + res.write('{}'); + res.end(); + }, + (err, client, close) => { + if (err) { + return callback(err); + } + stripeClient = client; + closeServer = close; + return callback(); + } + ); + }); + afterEach(() => closeServer()); + + it('if explicitly undefined', (callback) => { + stripeClient.customers.create({stripeAccount: undefined}, (err) => { + closeServer(); + if (err) { + return callback(err); + } + expect(Object.keys(headers)).not.to.include('stripe-account'); + return callback(); + }); + }); + + it('if explicitly null', (callback) => { + stripeClient.customers.create({stripeAccount: null}, (err) => { + closeServer(); + if (err) { + return callback(err); + } + expect(Object.keys(headers)).not.to.include('stripe-account'); + return callback(); + }); + }); + }); + }); + + describe('context', () => { + describe('when passed in via the config object', () => { + let headers; + let stripeClient; + let closeServer; + beforeEach((callback) => { + getTestServerStripe( + { + stripeContext: 'ctx_123', + }, + (req, res) => { + headers = req.headers; + res.writeHeader(200); + res.write('{}'); + res.end(); + }, + (err, client, close) => { + if (err) { + return callback(err); + } + stripeClient = client; + closeServer = close; + return callback(); + } + ); + }); + afterEach(() => closeServer()); + it('is not sent on v1 call', (callback) => { + stripeClient.customers.create((err) => { + closeServer(); + if (err) { + return callback(err); + } + expect(headers['stripe-context']).to.equal(undefined); + return callback(); + }); + }); + it('is respected', (callback) => { + stripeClient.v2.accounts.create((err) => { + closeServer(); + if (err) { + return callback(err); + } + expect(headers['stripe-context']).to.equal('ctx_123'); + return callback(); + }); + }); + it('can still be overridden per-request', (callback) => { + stripeClient.v2.accounts.create( + {name: 'llama'}, + {stripeContext: 'ctx_456'}, + (err) => { + closeServer(); + if (err) { + return callback(err); + } + expect(headers['stripe-context']).to.equal('ctx_456'); + return callback(); + } + ); + }); + }); }); describe('setMaxNetworkRetries', () => { @@ -545,12 +683,12 @@ describe('Stripe Module', function() { }); describe('when passed in via the config object', () => { - it('should default to 1 if a non-integer is passed', () => { + it('should default to 2 if a non-integer is passed', () => { const newStripe = Stripe(FAKE_API_KEY, { maxNetworkRetries: 'foo', }); - expect(newStripe.getMaxNetworkRetries()).to.equal(1); + expect(newStripe.getMaxNetworkRetries()).to.equal(2); expect(() => { Stripe(FAKE_API_KEY, { @@ -572,7 +710,7 @@ describe('Stripe Module', function() { it('should use the default', () => { const newStripe = Stripe(FAKE_API_KEY); - expect(newStripe.getMaxNetworkRetries()).to.equal(1); + expect(newStripe.getMaxNetworkRetries()).to.equal(2); }); }); }); @@ -584,4 +722,147 @@ describe('Stripe Module', function() { expect(newStripe.VERSION).to.equal(Stripe.PACKAGE_VERSION); }); }); + + describe('createRequestSigningAuthenticator', () => { + let oldDate; + beforeEach(() => { + oldDate = Date.now; + Date.now = (): number => 123456789000; + }); + + afterEach(() => { + Date.now = oldDate; + }); + + it('authenticator applies signature for POST requests', async () => { + const signatureBases = []; + const authenticator = Stripe.createRequestSigningAuthenticator( + 'keyid', + (signatureBase) => { + signatureBases.push(signatureBase); + return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); + } + ); + + const request = { + method: 'POST', + body: '{"string":"String!"}', + headers: {'Content-Type': 'application/json'}, + }; + + await authenticator(request); + + expect(new TextDecoder().decode(signatureBases[0])).to.equal( + '"content-type": application/json\n' + + '"content-digest": sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:\n' + + '"stripe-context": \n' + + '"stripe-account": \n' + + '"authorization": STRIPE-V2-SIG keyid\n' + + '"@signature-params": ("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");created=123456789' + ); + expect(request.headers['Signature-Input']).to.equal( + 'sig1=("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");' + + 'created=123456789' + ); + expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); + expect(request.headers['Content-Digest']).to.equal( + 'sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:' + ); + expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); + expect(request.headers['Content-Type']).to.equal('application/json'); + }); + + it(`authenticator applies signature for DELETE requests`, async () => { + const signatureBases = []; + const authenticator = Stripe.createRequestSigningAuthenticator( + 'keyid', + (signatureBase) => { + signatureBases.push(signatureBase); + return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); + } + ); + + const request = { + method: 'DELETE', + body: null, + headers: {'Content-Type': 'application/json'}, + }; + + await authenticator(request); + + expect(new TextDecoder().decode(signatureBases[0])).to.equal( + '"content-type": application/json\n' + + '"content-digest": sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:\n' + + '"stripe-context": \n' + + '"stripe-account": \n' + + '"authorization": STRIPE-V2-SIG keyid\n' + + '"@signature-params": ("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");created=123456789' + ); + expect(request.headers['Signature-Input']).to.equal( + 'sig1=("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");' + + 'created=123456789' + ); + expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); + expect(request.headers['Content-Digest']).to.equal( + 'sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:' + ); + expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); + expect(request.headers['Content-Type']).to.equal('application/json'); + }); + + it('authenticator applies signature for GET requests', async () => { + const signatureBases = []; + const authenticator = Stripe.createRequestSigningAuthenticator( + 'keyid', + (signatureBase) => { + signatureBases.push(signatureBase); + return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); + } + ); + + const request = { + method: 'GET', + headers: {}, + }; + + await authenticator(request); + + expect(new TextDecoder().decode(signatureBases[0])).to.equal( + '"stripe-context": \n' + + '"stripe-account": \n' + + '"authorization": STRIPE-V2-SIG keyid\n' + + '"@signature-params": ("stripe-context" "stripe-account" "authorization");created=123456789' + ); + expect(request.headers['Signature-Input']).to.equal( + 'sig1=("stripe-context" "stripe-account" "authorization");' + + 'created=123456789' + ); + expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); + expect(request.headers['Content-Digest']).to.equal(undefined); + expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); + }); + }); + + describe('parseThinEvent', () => { + const secret = 'whsec_test_secret'; + + it('can parse event from JSON payload', () => { + const payload = JSON.stringify({event_type: 'account.created'}); + const header = stripe.webhooks.generateTestHeaderString({ + payload, + secret, + }); + const event = stripe.parseThinEvent(payload, header, secret); + + expect(event.event_type).to.equal('account.created'); + }); + + it('throws an error for invalid signatures', () => { + const payload = JSON.stringify({event_type: 'account.created'}); + + expect(() => { + stripe.parseThinEvent(payload, 'bad sigheader', secret); + }).to.throw(StripeSignatureVerificationError); + }); + }); }); diff --git a/test/testUtils.ts b/test/testUtils.ts index 4d798ec6f7..454b273b46 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -8,6 +8,7 @@ import {NodePlatformFunctions} from '../src/platform/NodePlatformFunctions.js'; import {RequestSender} from '../src/RequestSender.js'; import {createStripe} from '../src/stripe.core.js'; import { + RequestAuthenticator, RequestCallback, RequestData, RequestDataProcessor, @@ -70,7 +71,7 @@ export const getStripeMockClient = (): StripeClient => { path: string, method: string, headers: RequestHeaders, - requestData: RequestData, + requestData: string, _protocol: string, timeout: number ): Promise { @@ -115,8 +116,8 @@ export const getMockStripe = ( host: string | null, path: string, data: RequestData, - auth: string | null, - options: RequestOptions = {}, + authenticator: RequestAuthenticator, + options: RequestOptions = {} as any, usage: Array, callback: RequestCallback, requestDataProcessor: RequestDataProcessor | null = null @@ -126,7 +127,7 @@ export const getMockStripe = ( host, path, data, - auth, + authenticator, options, usage, callback, @@ -170,8 +171,8 @@ export const getSpyableStripe = ( host: string | null, path: string, data: RequestData, - auth: string | null, - options: RequestOptions = {}, + authenticator: RequestAuthenticator, + options: RequestOptions = {} as any, usage: Array = [], callback: RequestCallback, requestDataProcessor: RequestDataProcessor | null = null @@ -183,6 +184,7 @@ export const getSpyableStripe = ( headers: RequestHeaders; settings: RequestSettings; auth?: string; + authenticator?: RequestAuthenticator; host?: string; usage?: Array; }; @@ -196,8 +198,13 @@ export const getSpyableStripe = ( if (usage && usage.length > 1) { req.usage = usage; } - if (auth) { - req.auth = auth; + if (authenticator) { + // Extract API key from the api-key authenticator + if ((authenticator as any)._apiKey) { + req.auth = (authenticator as any)._apiKey; + } else { + req.authenticator = authenticator; + } } if (host) { req.host = host; @@ -222,7 +229,7 @@ export const getSpyableStripe = ( host, path, data, - auth, + authenticator, options, usage, callback, diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 248d8ee92b..b2437d9c76 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -28,10 +28,10 @@ describe('utils', () => { }); }); - describe('stringifyRequestData', () => { + describe('queryStringifyRequestData', () => { it('Handles basic types', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ a: 1, b: 'foo', }) @@ -40,7 +40,7 @@ describe('utils', () => { it('Handles Dates', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ date: new Date('2009-02-13T23:31:30Z'), created: { gte: new Date('2009-02-13T23:31:30Z'), @@ -58,7 +58,7 @@ describe('utils', () => { it('Handles deeply nested object', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ a: { b: { c: { @@ -72,7 +72,7 @@ describe('utils', () => { it('Handles arrays of objects', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ a: [{b: 'c'}, {b: 'd'}], }) ).to.equal('a[0][b]=c&a[1][b]=d'); @@ -80,7 +80,7 @@ describe('utils', () => { it('Handles indexed arrays', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ a: { 0: {b: 'c'}, 1: {b: 'd'}, @@ -91,7 +91,7 @@ describe('utils', () => { it('Creates a string from an object, handling shallow nested objects', () => { expect( - utils.stringifyRequestData({ + utils.queryStringifyRequestData({ test: 1, foo: 'baz', somethingElse: '::""%&', @@ -110,6 +110,17 @@ describe('utils', () => { ].join('&') ); }); + + it('Handles v2 arrays', () => { + expect( + utils.queryStringifyRequestData( + { + include: ['a', 'b'], + }, + 'v2' + ) + ).to.equal('include=a&include=b'); + }); }); describe('protoExtend', () => { @@ -183,7 +194,6 @@ describe('utils', () => { describe('getOptsFromArgs', () => { it('handles an empty list', () => { expect(utils.getOptionsFromArgs([])).to.deep.equal({ - auth: null, host: null, headers: {}, settings: {}, @@ -193,7 +203,6 @@ describe('utils', () => { it('handles an list with no object', () => { const args = [1, 3]; expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: null, host: null, headers: {}, settings: {}, @@ -204,7 +213,6 @@ describe('utils', () => { it('ignores a non-options object', () => { const args = [{foo: 'bar'}]; expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: null, host: null, headers: {}, settings: {}, @@ -214,30 +222,33 @@ describe('utils', () => { it('parses an api key', () => { const args = ['sk_test_iiiiiiiiiiiiiiiiiiiiiiii']; - expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii', + const options = utils.getOptionsFromArgs(args); + expect(options).to.deep.contain({ host: null, headers: {}, settings: {}, }); expect(args.length).to.equal(0); + expect(options.authenticator._apiKey).to.equal( + 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii' + ); }); it('assumes any string is an api key', () => { const args = ['yolo']; - expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: 'yolo', + const options = utils.getOptionsFromArgs(args); + expect(options).to.deep.contain({ host: null, headers: {}, settings: {}, }); expect(args.length).to.equal(0); + expect(options.authenticator._apiKey).to.equal('yolo'); }); it('parses an idempotency key', () => { const args = [{foo: 'bar'}, {idempotencyKey: 'foo'}]; expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: null, host: null, headers: {'Idempotency-Key': 'foo'}, settings: {}, @@ -248,7 +259,6 @@ describe('utils', () => { it('parses an api version', () => { const args = [{foo: 'bar'}, {apiVersion: '2003-03-30'}]; expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: null, host: null, headers: {'Stripe-Version': '2003-03-30'}, settings: {}, @@ -265,8 +275,8 @@ describe('utils', () => { apiVersion: '2010-01-10', }, ]; - expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii', + const options = utils.getOptionsFromArgs(args); + expect(options).to.deep.contains({ host: null, headers: { 'Idempotency-Key': 'foo', @@ -275,6 +285,9 @@ describe('utils', () => { settings: {}, }); expect(args.length).to.equal(1); + expect(options.authenticator._apiKey).to.equal( + 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii' + ); }); it('parses an idempotency key and api key and api version', () => { @@ -285,8 +298,8 @@ describe('utils', () => { apiVersion: 'hunter2', }, ]; - expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii', + const options = utils.getOptionsFromArgs(args); + expect(options).to.deep.contains({ host: null, headers: { 'Idempotency-Key': 'foo', @@ -295,6 +308,9 @@ describe('utils', () => { settings: {}, }); expect(args.length).to.equal(0); + expect(options.authenticator._apiKey).to.equal( + 'sk_test_iiiiiiiiiiiiiiiiiiiiiiii' + ); }); it('parses additional per-request settings', () => { @@ -306,7 +322,6 @@ describe('utils', () => { ]; expect(utils.getOptionsFromArgs(args)).to.deep.equal({ - auth: null, host: null, headers: {}, settings: { diff --git a/types/Errors.d.ts b/types/Errors.d.ts index 0b3a10d639..7f05e43539 100644 --- a/types/Errors.d.ts +++ b/types/Errors.d.ts @@ -1,5 +1,6 @@ declare module 'stripe' { namespace Stripe { + // rawErrorTypeEnum: The beginning of the section generated from our OpenAPI spec export type RawErrorType = | 'card_error' | 'invalid_request_error' @@ -7,10 +8,13 @@ declare module 'stripe' { | 'idempotency_error' | 'rate_limit_error' | 'authentication_error' - | 'invalid_grant'; + | 'invalid_grant' + | 'temporary_session_expired'; + // rawErrorTypeEnum: The end of the section generated from our OpenAPI spec export type StripeRawError = { message?: string; + userMessage?: string; type: RawErrorType; @@ -32,27 +36,35 @@ declare module 'stripe' { }; namespace errors { + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'card_error'} ): StripeCardError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'invalid_request_error'} ): StripeInvalidRequestError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'api_error'} ): StripeAPIError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'authentication_error'} ): StripeAuthenticationError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'rate_limit_error'} ): StripeRateLimitError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'idempotency_error'} ): StripeIdempotencyError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: 'invalid_grant'} ): StripeInvalidGrantError; + /** @deprecated Not for external use. */ function generate( rawError: StripeRawError & {type: RawErrorType} ): StripeError; @@ -60,27 +72,35 @@ declare module 'stripe' { class StripeError extends Error { constructor(rawError: StripeRawError); + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'card_error'} ): StripeCardError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'invalid_request_error'} ): StripeInvalidRequestError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'api_error'} ): StripeAPIError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'authentication_error'} ): StripeAuthenticationError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'rate_limit_error'} ): StripeRateLimitError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'idempotency_error'} ): StripeIdempotencyError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: 'invalid_grant'} ): StripeInvalidGrantError; + /** @deprecated Not for external use. */ static generate( rawError: StripeRawError & {type: RawErrorType} ): StripeError; @@ -91,6 +111,7 @@ declare module 'stripe' { */ readonly message: string; + // errorClassNameEnum: The beginning of the section generated from our OpenAPI spec readonly type: | 'StripeError' | 'StripeCardError' @@ -102,7 +123,9 @@ declare module 'stripe' { | 'StripeConnectionError' | 'StripeSignatureVerificationError' | 'StripeIdempotencyError' - | 'StripeInvalidGrantError'; + | 'StripeInvalidGrantError' + | 'TemporarySessionExpiredError'; + // errorClassNameEnum: The end of the section generated from our OpenAPI spec /** * See the "error types" section at https://stripe.com/docs/api/errors @@ -245,6 +268,13 @@ declare module 'stripe' { readonly type: 'StripeInvalidGrantError'; readonly rawType: 'invalid_grant'; } + + // errorClassDefinitions: The beginning of the section generated from our OpenAPI spec + export class TemporarySessionExpiredError extends StripeError { + readonly type: 'TemporarySessionExpiredError'; + readonly rawType: 'temporary_session_expired'; + } + // errorClassDefinitions: The end of the section generated from our OpenAPI spec } } } diff --git a/types/EventTypes.d.ts b/types/EventTypes.d.ts index 54f2e9c05f..d22e466207 100644 --- a/types/EventTypes.d.ts +++ b/types/EventTypes.d.ts @@ -240,7 +240,9 @@ declare module 'stripe' { | TreasuryReceivedCreditFailedEvent | TreasuryReceivedCreditSucceededEvent | TreasuryReceivedDebitCreatedEvent; + } + namespace Stripe { /** * Occurs whenever a user authorizes an application. Sent to the related application only. */ diff --git a/types/V2/Billing/MeterEventAdjustmentV2S.d.ts b/types/V2/Billing/MeterEventAdjustmentV2S.d.ts new file mode 100644 index 0000000000..b816893e3c --- /dev/null +++ b/types/V2/Billing/MeterEventAdjustmentV2S.d.ts @@ -0,0 +1,65 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + /** + * The MeterEventAdjustmentV2 object. + */ + interface MeterEventAdjustmentV2 { + /** + * The unique id of this meter event adjustment. + */ + id: string; + + /** + * String representing the object's type. Objects of the same type share the same value of the object field. + */ + object: 'billing.meter_event_adjustment_v2'; + + /** + * Specifies which event to cancel. + */ + cancel: MeterEventAdjustmentV2.Cancel; + + /** + * The time the adjustment was created. + */ + created: string; + + /** + * The name of the meter event. Corresponds with the `event_name` field on a meter. + */ + event_name: string; + + /** + * Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + */ + livemode: boolean; + + /** + * The meter event adjustment's status. + */ + status: MeterEventAdjustmentV2.Status; + + /** + * Specifies whether to cancel a single event or a range of events for a time period. Time period cancellation is not supported yet. + */ + type: 'cancel'; + } + + namespace MeterEventAdjustmentV2 { + interface Cancel { + /** + * Unique identifier for the event. You can only cancel events within 24 hours of Stripe receiving them. + */ + identifier: string; + } + + type Status = 'complete' | 'pending'; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventAdjustmentsResource.d.ts b/types/V2/Billing/MeterEventAdjustmentsResource.d.ts new file mode 100644 index 0000000000..696df61388 --- /dev/null +++ b/types/V2/Billing/MeterEventAdjustmentsResource.d.ts @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + interface MeterEventAdjustmentCreateParams { + /** + * Specifies which event to cancel. + */ + cancel: MeterEventAdjustmentCreateParams.Cancel; + + /** + * The name of the meter event. Corresponds with the `event_name` field on a meter. + */ + event_name: string; + + /** + * Specifies whether to cancel a single event or a range of events for a time period. Time period cancellation is not supported yet. + */ + type: 'cancel'; + } + + namespace MeterEventAdjustmentCreateParams { + interface Cancel { + /** + * Unique identifier for the event. You can only cancel events within 24 hours of Stripe receiving them. + */ + identifier: string; + } + } + } + + namespace Billing { + class MeterEventAdjustmentsResource { + /** + * Creates a meter event adjustment to cancel a previously sent meter event. + */ + create( + params: MeterEventAdjustmentCreateParams, + options?: RequestOptions + ): Promise>; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventSessionResource.d.ts b/types/V2/Billing/MeterEventSessionResource.d.ts new file mode 100644 index 0000000000..be354bdd53 --- /dev/null +++ b/types/V2/Billing/MeterEventSessionResource.d.ts @@ -0,0 +1,26 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + interface MeterEventSessionCreateParams {} + } + + namespace Billing { + class MeterEventSessionResource { + /** + * Creates a meter event session to send usage on the high-throughput meter event stream. + */ + create( + params?: MeterEventSessionCreateParams, + options?: RequestOptions + ): Promise>; + create( + options?: RequestOptions + ): Promise>; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventSessions.d.ts b/types/V2/Billing/MeterEventSessions.d.ts new file mode 100644 index 0000000000..6259622644 --- /dev/null +++ b/types/V2/Billing/MeterEventSessions.d.ts @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + /** + * The MeterEventSession object. + */ + interface MeterEventSession { + /** + * The unique id of this auth session. + */ + id: string; + + /** + * String representing the object's type. Objects of the same type share the same value of the object field. + */ + object: 'billing.meter_event_session'; + + /** + * The authentication token for this session. Use this token when calling the + * high-throughput meter event API. + */ + authentication_token: string; + + /** + * The creation time of this session. + */ + created: string; + + /** + * The time at which this session will expire. + */ + expires_at: string; + + /** + * Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + */ + livemode: boolean; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventStreamResource.d.ts b/types/V2/Billing/MeterEventStreamResource.d.ts new file mode 100644 index 0000000000..56f8cf51f1 --- /dev/null +++ b/types/V2/Billing/MeterEventStreamResource.d.ts @@ -0,0 +1,62 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + interface MeterEventStreamCreateParams { + /** + * List of meter events to include in the request. + */ + events: Array; + } + + namespace MeterEventStreamCreateParams { + interface Event { + /** + * The name of the meter event. Corresponds with the `event_name` field on a meter. + */ + event_name: string; + + /** + * A unique identifier for the event. If not provided, one will be generated. + * We recommend using a globally unique identifier for this. We'll enforce + * uniqueness within a rolling 24 hour period. + */ + identifier?: string; + + /** + * The payload of the event. This must contain the fields corresponding to a meter's + * `customer_mapping.event_payload_key` (default is `stripe_customer_id`) and + * `value_settings.event_payload_key` (default is `value`). Read more about + * the + * [payload](https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage#payload-key-overrides). + */ + payload: { + [key: string]: string; + }; + + /** + * The time of the event. Must be within the past 35 calendar days or up to + * 5 minutes in the future. Defaults to current timestamp if not specified. + */ + timestamp?: string; + } + } + } + + namespace Billing { + class MeterEventStreamResource { + /** + * Sends a meter event for asynchronous processing. Supports higher rate limits. Requires a meter event session for authentication. + * @throws Stripe.TemporarySessionExpiredError + */ + create( + params: MeterEventStreamCreateParams, + options?: RequestOptions + ): Promise; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventV2S.d.ts b/types/V2/Billing/MeterEventV2S.d.ts new file mode 100644 index 0000000000..1660b2ea41 --- /dev/null +++ b/types/V2/Billing/MeterEventV2S.d.ts @@ -0,0 +1,54 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + /** + * Fix me empty_doc_string. + */ + interface MeterEventV2 { + /** + * String representing the object's type. Objects of the same type share the same value of the object field. + */ + object: 'billing.meter_event_v2'; + + /** + * The creation time of this meter event. + */ + created: string; + + /** + * The name of the meter event. Corresponds with the `event_name` field on a meter. + */ + event_name: string; + + /** + * A unique identifier for the event. If not provided, one will be generated. We recommend using a globally unique identifier for this. We'll enforce uniqueness within a rolling 24 hour period. + */ + identifier: string; + + /** + * Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + */ + livemode: boolean; + + /** + * The payload of the event. This must contain the fields corresponding to a meter's + * `customer_mapping.event_payload_key` (default is `stripe_customer_id`) and + * `value_settings.event_payload_key` (default is `value`). Read more about the payload. + */ + payload: { + [key: string]: string; + }; + + /** + * The time of the event. Must be within the past 35 calendar days or up to + * 5 minutes in the future. Defaults to current timestamp if not specified. + */ + timestamp: string; + } + } + } + } +} diff --git a/types/V2/Billing/MeterEventsResource.d.ts b/types/V2/Billing/MeterEventsResource.d.ts new file mode 100644 index 0000000000..db15e41043 --- /dev/null +++ b/types/V2/Billing/MeterEventsResource.d.ts @@ -0,0 +1,52 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Billing { + interface MeterEventCreateParams { + /** + * The name of the meter event. Corresponds with the `event_name` field on a meter. + */ + event_name: string; + + /** + * The payload of the event. This must contain the fields corresponding to a meter's + * `customer_mapping.event_payload_key` (default is `stripe_customer_id`) and + * `value_settings.event_payload_key` (default is `value`). Read more about + * the + * [payload](https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage#payload-key-overrides). + */ + payload: { + [key: string]: string; + }; + + /** + * A unique identifier for the event. If not provided, one will be generated. + * We recommend using a globally unique identifier for this. We'll enforce + * uniqueness within a rolling 24 hour period. + */ + identifier?: string; + + /** + * The time of the event. Must be within the past 35 calendar days or up to + * 5 minutes in the future. Defaults to current timestamp if not specified. + */ + timestamp?: string; + } + } + + namespace Billing { + class MeterEventsResource { + /** + * Creates a meter event. Validates the event synchronously. + */ + create( + params: MeterEventCreateParams, + options?: RequestOptions + ): Promise>; + } + } + } + } +} diff --git a/types/V2/BillingResource.d.ts b/types/V2/BillingResource.d.ts new file mode 100644 index 0000000000..a57eee05c7 --- /dev/null +++ b/types/V2/BillingResource.d.ts @@ -0,0 +1,14 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + class BillingResource { + meterEventSession: Stripe.V2.Billing.MeterEventSessionResource; + meterEventAdjustments: Stripe.V2.Billing.MeterEventAdjustmentsResource; + meterEventStream: Stripe.V2.Billing.MeterEventStreamResource; + meterEvents: Stripe.V2.Billing.MeterEventsResource; + } + } + } +} diff --git a/types/V2/Core/EventsResource.d.ts b/types/V2/Core/EventsResource.d.ts new file mode 100644 index 0000000000..e74b6bee42 --- /dev/null +++ b/types/V2/Core/EventsResource.d.ts @@ -0,0 +1,49 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Core { + interface EventRetrieveParams {} + } + + namespace Core { + interface EventListParams { + /** + * Primary object ID used to retrieve related events. + */ + object_id: string; + + limit?: number; + + page?: string; + } + } + + namespace Core { + class EventsResource { + /** + * Retrieves the details of an event. + */ + retrieve( + id: string, + params?: EventRetrieveParams, + options?: RequestOptions + ): Promise>; + retrieve( + id: string, + options?: RequestOptions + ): Promise>; + + /** + * List events, going back up to 30 days. + */ + list( + params: EventListParams, + options?: RequestOptions + ): ApiListPromise; + } + } + } + } +} diff --git a/types/V2/CoreResource.d.ts b/types/V2/CoreResource.d.ts new file mode 100644 index 0000000000..f65f8a4b98 --- /dev/null +++ b/types/V2/CoreResource.d.ts @@ -0,0 +1,11 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + class CoreResource { + events: Stripe.V2.Core.EventsResource; + } + } + } +} diff --git a/types/V2/EventDestinations.d.ts b/types/V2/EventDestinations.d.ts new file mode 100644 index 0000000000..aa2409f999 --- /dev/null +++ b/types/V2/EventDestinations.d.ts @@ -0,0 +1,154 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + /** + * The EventDestination object. + */ + interface EventDestination { + /** + * Unique identifier for the object. + */ + id: string; + + /** + * String representing the object's type. Objects of the same type share the same value of the object field. + */ + object: 'event_destination'; + + /** + * Amazon EventBridge configuration. + */ + amazon_eventbridge: EventDestination.AmazonEventbridge | null; + + /** + * Time at which the object was created. + */ + created: string; + + /** + * An optional description of what the event destination is used for. + */ + description: string; + + /** + * The list of events to enable for this endpoint. + */ + enabled_events: Array; + + /** + * Closed Enum. Namespace of events being subscribed to. + */ + event_namespace: EventDestination.EventNamespace; + + /** + * Open Enum. Where events should be routed from. + */ + events_from: Array | null; + + /** + * Metadata. + */ + metadata: Stripe.Metadata | null; + + /** + * Event destination name. + */ + name: string; + + /** + * Closed Enum. Status. It can be set to either enabled or disabled. + */ + status: EventDestination.Status; + + /** + * Additional information about event destination status. + */ + status_details: EventDestination.StatusDetails | null; + + /** + * Closed Enum. Event destination type. + */ + type: EventDestination.Type; + + /** + * Time at which the object was last updated. + */ + updated: string; + + /** + * If using the v1 event namespace, the API version events are rendered as. + */ + v1_api_version: string | null; + + /** + * Webhook endpoint configuration. + */ + webhook_endpoint: EventDestination.WebhookEndpoint | null; + } + + namespace EventDestination { + interface AmazonEventbridge { + /** + * The AWS account ID. + */ + aws_account_id: string; + + /** + * The ARN of the AWS event source. + */ + aws_event_source_arn: string; + + /** + * Closed Enum. The state of the AWS event source. + */ + aws_event_source_status: AmazonEventbridge.AwsEventSourceStatus; + } + + namespace AmazonEventbridge { + type AwsEventSourceStatus = + | 'active' + | 'deleted' + | 'pending' + | 'unknown'; + } + + type EventNamespace = 'v1' | 'v2'; + + type EventsFrom = 'accounts' | 'self'; + + type Status = 'disabled' | 'enabled'; + + interface StatusDetails { + /** + * Details about why the event destination has been disabled. + */ + disabled: StatusDetails.Disabled | null; + } + + namespace StatusDetails { + interface Disabled { + /** + * Closed Enum. Reason event destination has been disabled. + */ + reason: Disabled.Reason; + } + + namespace Disabled { + type Reason = 'no_aws_event_source_exists' | 'user'; + } + } + + type Type = 'amazon_eventbridge' | 'webhook_endpoint'; + + interface WebhookEndpoint { + /** + * The URL of the webhook endpoint. + */ + url: string; + } + } + } + } +} diff --git a/types/V2/EventTypes.d.ts b/types/V2/EventTypes.d.ts new file mode 100644 index 0000000000..d8f54e86fe --- /dev/null +++ b/types/V2/EventTypes.d.ts @@ -0,0 +1,224 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe.V2 { + export type Event = + | Stripe.Events.V1BillingMeterErrorReportTriggeredEvent + | Stripe.Events.V1BillingMeterNoMeterFoundEvent + | Stripe.Events.V2CoreEventDestinationPingEvent; + } + + namespace Stripe.Events { + /** + * This event occurs when an async usage event error report is generated. + */ + export interface V1BillingMeterErrorReportTriggeredEvent + extends V2.EventBase { + type: 'v1.billing.meter.error_report_triggered'; + // Retrieves data specific to this event. + data: V1BillingMeterErrorReportTriggeredEvent.Data; + // Retrieves the object associated with the event. + fetchRelatedObject(): Promise; + } + + namespace V1BillingMeterErrorReportTriggeredEvent { + export interface Data { + /** + * Extra field included in the event's `data` when fetched from /v2/events. + */ + developer_message_summary: string; + + /** + * This contains information about why meter error happens. + */ + reason: Data.Reason; + + /** + * The end of the window that is encapsulated by this summary. + */ + validation_end: string; + + /** + * The start of the window that is encapsulated by this summary. + */ + validation_start: string; + } + + namespace Data { + export interface Reason { + /** + * The total error count within this window. + */ + error_count: number; + + /** + * The error details. + */ + error_types: Array; + } + + namespace Reason { + export interface ErrorType { + /** + * Open Enum. + */ + code: ErrorType.Code; + + /** + * The number of errors of this type. + */ + error_count: number; + + /** + * A list of sample errors of this type. + */ + sample_errors: Array; + } + + namespace ErrorType { + export type Code = + | 'archived_meter' + | 'meter_event_customer_not_found' + | 'meter_event_dimension_count_too_high' + | 'meter_event_invalid_value' + | 'meter_event_no_customer_defined' + | 'missing_dimension_payload_keys' + | 'no_meter' + | 'timestamp_in_future' + | 'timestamp_too_far_in_past'; + + export interface SampleError { + /** + * The error message. + */ + error_message: string; + + /** + * The request causes the error. + */ + request: SampleError.Request; + } + + namespace SampleError { + export interface Request { + /** + * The request idempotency key. + */ + identifier: string; + } + } + } + } + } + } + + /** + * This event occurs when an async usage event is missing a meter. + */ + export interface V1BillingMeterNoMeterFoundEvent extends V2.EventBase { + type: 'v1.billing.meter.no_meter_found'; + // Retrieves data specific to this event. + data: V1BillingMeterNoMeterFoundEvent.Data; + } + + namespace V1BillingMeterNoMeterFoundEvent { + export interface Data { + /** + * Extra field included in the event's `data` when fetched from /v2/events. + */ + developer_message_summary: string; + + /** + * This contains information about why meter error happens. + */ + reason: Data.Reason; + + /** + * The end of the window that is encapsulated by this summary. + */ + validation_end: string; + + /** + * The start of the window that is encapsulated by this summary. + */ + validation_start: string; + } + + namespace Data { + export interface Reason { + /** + * The total error count within this window. + */ + error_count: number; + + /** + * The error details. + */ + error_types: Array; + } + + namespace Reason { + export interface ErrorType { + /** + * Open Enum. + */ + code: ErrorType.Code; + + /** + * The number of errors of this type. + */ + error_count: number; + + /** + * A list of sample errors of this type. + */ + sample_errors: Array; + } + + namespace ErrorType { + export type Code = + | 'archived_meter' + | 'meter_event_customer_not_found' + | 'meter_event_dimension_count_too_high' + | 'meter_event_invalid_value' + | 'meter_event_no_customer_defined' + | 'missing_dimension_payload_keys' + | 'no_meter' + | 'timestamp_in_future' + | 'timestamp_too_far_in_past'; + + export interface SampleError { + /** + * The error message. + */ + error_message: string; + + /** + * The request causes the error. + */ + request: SampleError.Request; + } + + namespace SampleError { + export interface Request { + /** + * The request idempotency key. + */ + identifier: string; + } + } + } + } + } + } + + /** + * A ping event used to test the connection to an event destination. + */ + export interface V2CoreEventDestinationPingEvent extends V2.EventBase { + type: 'v2.core.event_destination.ping'; + // Retrieves the object associated with the event. + fetchRelatedObject(): Promise; + } + } +} diff --git a/types/V2/Events.d.ts b/types/V2/Events.d.ts new file mode 100644 index 0000000000..5808e8a158 --- /dev/null +++ b/types/V2/Events.d.ts @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + namespace V2 { + namespace Event { + interface Reason { + /** + * Open Enum. Event reason type. + */ + type: 'request'; + + /** + * Information on the API request that instigated the event. + */ + request: Reason.Request | null; + } + + namespace Reason { + interface Request { + /** + * ID of the API request that caused the event. + */ + id: string; + + /** + * The idempotency key transmitted during the request. + */ + idempotency_key: string; + } + } + } + + /** + * The Event object. + */ + interface EventBase { + /** + * Unique identifier for the event. + */ + id: string; + + /** + * String representing the object's type. Objects of the same type share the same value of the object field. + */ + object: 'event'; + + /** + * Authentication context needed to fetch the event or related object. + */ + context: string | null; + + /** + * Time at which the object was created. + */ + created: string; + + /** + * Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + */ + livemode: boolean; + + /** + * Reason for the event. + */ + reason: Event.Reason | null; + + /** + * The type of the event. + */ + type: string; + } + } + } +} diff --git a/types/V2Resource.d.ts b/types/V2Resource.d.ts new file mode 100644 index 0000000000..b882341036 --- /dev/null +++ b/types/V2Resource.d.ts @@ -0,0 +1,10 @@ +// File generated from our OpenAPI spec + +declare module 'stripe' { + namespace Stripe { + class V2Resource { + billing: Stripe.V2.BillingResource; + core: Stripe.V2.CoreResource; + } + } +} diff --git a/types/index.d.ts b/types/index.d.ts index dea69dcf12..41f9bfc7e0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -124,6 +124,14 @@ /// /// /// +/// +/// +/// +/// +/// +/// +/// +/// /// /// /// @@ -262,6 +270,11 @@ /// /// /// +/// +/// +/// +/// +/// /// // Imports: The end of the section generated from our OpenAPI spec @@ -333,6 +346,7 @@ declare module 'stripe' { tokens: Stripe.TokensResource; topups: Stripe.TopupsResource; transfers: Stripe.TransfersResource; + v2: Stripe.V2Resource; webhookEndpoints: Stripe.WebhookEndpointsResource; apps: { secrets: Stripe.Apps.SecretsResource; From 88d3b936675ad31d4b9a30365fd5fc6306c4f64e Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Tue, 24 Sep 2024 15:26:43 -0700 Subject: [PATCH 02/20] Fix tests to use the billing endpoint --- test/Error.spec.ts | 11 +++--- test/RequestSender.spec.ts | 71 ++++++++++++++++++-------------------- test/stripe.spec.ts | 4 +-- 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/test/Error.spec.ts b/test/Error.spec.ts index 2085209338..4a3774ee59 100644 --- a/test/Error.spec.ts +++ b/test/Error.spec.ts @@ -34,13 +34,12 @@ describe('Error', () => { ).to.be.instanceOf(Error.StripeCardError); expect( Error.generateV2({ - type: 'insufficient_funds', - code: 'outbound_payment_insufficient_funds', + code: 'invalid_fields', }) - ).to.be.instanceOf(Error.InsufficientFundsError); - expect(Error.generateV2({type: 'blocked_by_stripe'})).to.be.instanceOf( - Error.BlockedByStripeError - ); + ).to.be.instanceOf(Error.StripeInvalidRequestError); + expect( + Error.generateV2({type: 'temporary_session_expired'}) + ).to.be.instanceOf(Error.TemporarySessionExpiredError); expect(Error.generateV2({code: 'invalid_fields'})).to.be.instanceOf( Error.StripeInvalidRequestError diff --git a/test/RequestSender.spec.ts b/test/RequestSender.spec.ts index b6e4718891..21bdd2ca6c 100644 --- a/test/RequestSender.spec.ts +++ b/test/RequestSender.spec.ts @@ -11,6 +11,7 @@ import { StripePermissionError, StripeRateLimitError, StripeUnknownError, + TemporarySessionExpiredError, } from '../src/Error.js'; import {RequestSender} from '../src/RequestSender.js'; import {ApiVersion, PreviewVersion} from '../src/apiVersion.js'; @@ -320,7 +321,7 @@ describe('RequestSender', () => { it('encodes the body in POST requests as JSON for v2', (done) => { const options = { host: stripe.getConstant('DEFAULT_HOST'), - path: '/v2/accounts', + path: '/v2/billing/meter_event_session', data: { name: 'llama', }, @@ -340,16 +341,19 @@ describe('RequestSender', () => { .post(options.path, options.body) .reply(200, '{}'); - realStripe.v2.accounts.create(options.data, (err, response) => { - done(err); - scope.done(); - }); + realStripe.v2.billing.meterEventSession.create( + options.data, + (err, response) => { + done(err); + scope.done(); + } + ); }); it('encodes null values in the body in POST correctly for v2', (done) => { const options = { host: stripe.getConstant('DEFAULT_HOST'), - path: '/v2/accounts', + path: '/v2/billing/meter_event_session', data: { name: null, }, @@ -369,23 +373,26 @@ describe('RequestSender', () => { .post(options.path, options.body) .reply(200, '{}'); - realStripe.v2.accounts.create(options.data, (err, response) => { - done(err); - scope.done(); - }); + realStripe.v2.billing.meterEventSession.create( + options.data, + (err, response) => { + done(err); + scope.done(); + } + ); }); it('encodes data for GET requests as query params for v2', (done) => { const host = stripe.getConstant('DEFAULT_HOST'); const scope = nock(`https://${host}`) .get( - `/v2/accounts/acc_123?include=defaults&include=configuration`, + `/v2/core/events/event_123?include=defaults&include=configuration`, '' ) .reply(200, '{}'); - realStripe.v2.accounts.retrieve( - 'acc_123', + realStripe.v2.core.events.retrieve( + 'event_123', {include: ['defaults', 'configuration']}, (err, response) => { done(err); @@ -426,7 +433,7 @@ describe('RequestSender', () => { it('encodes Date objects in POST requests as JSON for v2', (done) => { const options = { host: stripe.getConstant('DEFAULT_HOST'), - path: '/v2/accounts', + path: '/v2/billing/meter_event_session', data: { created: new Date('2009-02-13T23:31:30Z'), }, @@ -446,10 +453,13 @@ describe('RequestSender', () => { .post(options.path, options.body) .reply(200, '{}'); - realStripe.v2.accounts.create(options.data, (err, response) => { - done(err); - scope.done(); - }); + realStripe.v2.billing.meterEventSession.create( + options.data, + (err, response) => { + done(err); + scope.done(); + } + ); }); it('allows overriding host', (done) => { @@ -483,19 +493,6 @@ describe('RequestSender', () => { scope.done(); }); }); - it('sends current v2 version when apiMode is v2', (done) => { - const host = stripe.getConstant('DEFAULT_HOST'); - const scope = nock(`https://${host}`, { - reqheaders: {'Stripe-Version': PreviewVersion}, - }) - .get('/v2/accounts') - .reply(200, '{}'); - - realStripe.v2.accounts.list((err, response) => { - done(err); - scope.done(); - }); - }); }); }); @@ -675,18 +672,18 @@ describe('RequestSender', () => { it('throws a v2 StripeError based on the underlying error "code" if apiMode is v2', (done) => { const error = { - type: 'insufficient_funds', + type: 'temporary_session_expired', message: 'you messed up', }; nock(`https://${options.host}`) - .post('/v2/accounts', {}) + .post('/v2/billing/meter_event_session', {}) .reply(400, { error, }); - realStripe.v2.accounts.create({}, (err) => { - expect(err).to.be.an.instanceOf(InsufficientFundsError); + realStripe.v2.billing.meterEventSession.create({}, (err) => { + expect(err).to.be.an.instanceOf(TemporarySessionExpiredError); expect(err.message).to.equal('you messed up'); done(); }); @@ -694,7 +691,7 @@ describe('RequestSender', () => { it('throws a v1 StripeError if apiMode is NOT v2', (done) => { const error = { - type: 'insufficient_funds', + type: 'temporary_session_expired', message: 'you messed up', }; @@ -707,7 +704,7 @@ describe('RequestSender', () => { realStripe.customers.create({}, (err) => { expect(err).to.be.an.instanceOf(StripeError); expect(err).to.be.an.instanceOf(StripeUnknownError); - expect(err).not.to.be.an.instanceOf(InsufficientFundsError); + expect(err).not.to.be.an.instanceOf(TemporarySessionExpiredError); expect(err.message).to.equal('you messed up'); expect(err.raw.message).to.equal('you messed up'); done(); diff --git a/test/stripe.spec.ts b/test/stripe.spec.ts index 836768c186..91e744079c 100644 --- a/test/stripe.spec.ts +++ b/test/stripe.spec.ts @@ -643,7 +643,7 @@ describe('Stripe Module', function() { }); }); it('is respected', (callback) => { - stripeClient.v2.accounts.create((err) => { + stripeClient.v2.billing.meterEventSession.create((err) => { closeServer(); if (err) { return callback(err); @@ -653,7 +653,7 @@ describe('Stripe Module', function() { }); }); it('can still be overridden per-request', (callback) => { - stripeClient.v2.accounts.create( + stripeClient.v2.billing.meterEventSession.create( {name: 'llama'}, {stripeContext: 'ctx_456'}, (err) => { From b4934d1cda18baf5fc87c5e4b54ee9b0781cceb7 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Tue, 24 Sep 2024 23:16:07 -0700 Subject: [PATCH 03/20] Pull latest changes from spec --- .../resources/generated_examples_test.spec.js | 2 +- types/V2/Billing/MeterEventAdjustmentV2S.d.ts | 2 +- .../V2/Billing/MeterEventSessionResource.d.ts | 2 +- types/V2/Billing/MeterEventV2S.d.ts | 2 +- types/V2/EventDestinations.d.ts | 154 ------------------ types/V2/EventTypes.d.ts | 12 +- types/V2/Events.d.ts | 2 +- types/index.d.ts | 1 - 8 files changed, 6 insertions(+), 171 deletions(-) delete mode 100644 types/V2/EventDestinations.d.ts diff --git a/test/resources/generated_examples_test.spec.js b/test/resources/generated_examples_test.spec.js index 721ecc434d..a3da0e5a43 100644 --- a/test/resources/generated_examples_test.spec.js +++ b/test/resources/generated_examples_test.spec.js @@ -573,7 +573,7 @@ describe('Generated tests', function() { method: 'GET', path: '/v2/core/events/ll_123', response: - '{"context":"context","created":"1970-01-12T21:42:34.472Z","id":"obj_123","livemode":true,"object":"event","reason":{"type":"request","request":{"id":"obj_123","idempotency_key":"idempotency_key"}},"type":"type"}', + '{"context":"context","created":"1970-01-12T21:42:34.472Z","id":"obj_123","livemode":true,"object":"v2.core.event","reason":{"type":"request","request":{"id":"obj_123","idempotency_key":"idempotency_key"}},"type":"type"}', }, ]); const event = await stripe.v2.core.events.retrieve('ll_123'); diff --git a/types/V2/Billing/MeterEventAdjustmentV2S.d.ts b/types/V2/Billing/MeterEventAdjustmentV2S.d.ts index b816893e3c..de99807156 100644 --- a/types/V2/Billing/MeterEventAdjustmentV2S.d.ts +++ b/types/V2/Billing/MeterEventAdjustmentV2S.d.ts @@ -16,7 +16,7 @@ declare module 'stripe' { /** * String representing the object's type. Objects of the same type share the same value of the object field. */ - object: 'billing.meter_event_adjustment_v2'; + object: 'v2.billing.meter_event_adjustment'; /** * Specifies which event to cancel. diff --git a/types/V2/Billing/MeterEventSessionResource.d.ts b/types/V2/Billing/MeterEventSessionResource.d.ts index be354bdd53..dadb405d8b 100644 --- a/types/V2/Billing/MeterEventSessionResource.d.ts +++ b/types/V2/Billing/MeterEventSessionResource.d.ts @@ -10,7 +10,7 @@ declare module 'stripe' { namespace Billing { class MeterEventSessionResource { /** - * Creates a meter event session to send usage on the high-throughput meter event stream. + * Creates a meter event session to send usage on the high-throughput meter event stream. Authentication tokens are only valid for 15 minutes, so you will need to create a new meter event session when your token expires. */ create( params?: MeterEventSessionCreateParams, diff --git a/types/V2/Billing/MeterEventV2S.d.ts b/types/V2/Billing/MeterEventV2S.d.ts index 1660b2ea41..c7033817d4 100644 --- a/types/V2/Billing/MeterEventV2S.d.ts +++ b/types/V2/Billing/MeterEventV2S.d.ts @@ -11,7 +11,7 @@ declare module 'stripe' { /** * String representing the object's type. Objects of the same type share the same value of the object field. */ - object: 'billing.meter_event_v2'; + object: 'v2.billing.meter_event'; /** * The creation time of this meter event. diff --git a/types/V2/EventDestinations.d.ts b/types/V2/EventDestinations.d.ts deleted file mode 100644 index aa2409f999..0000000000 --- a/types/V2/EventDestinations.d.ts +++ /dev/null @@ -1,154 +0,0 @@ -// File generated from our OpenAPI spec - -declare module 'stripe' { - namespace Stripe { - namespace V2 { - /** - * The EventDestination object. - */ - interface EventDestination { - /** - * Unique identifier for the object. - */ - id: string; - - /** - * String representing the object's type. Objects of the same type share the same value of the object field. - */ - object: 'event_destination'; - - /** - * Amazon EventBridge configuration. - */ - amazon_eventbridge: EventDestination.AmazonEventbridge | null; - - /** - * Time at which the object was created. - */ - created: string; - - /** - * An optional description of what the event destination is used for. - */ - description: string; - - /** - * The list of events to enable for this endpoint. - */ - enabled_events: Array; - - /** - * Closed Enum. Namespace of events being subscribed to. - */ - event_namespace: EventDestination.EventNamespace; - - /** - * Open Enum. Where events should be routed from. - */ - events_from: Array | null; - - /** - * Metadata. - */ - metadata: Stripe.Metadata | null; - - /** - * Event destination name. - */ - name: string; - - /** - * Closed Enum. Status. It can be set to either enabled or disabled. - */ - status: EventDestination.Status; - - /** - * Additional information about event destination status. - */ - status_details: EventDestination.StatusDetails | null; - - /** - * Closed Enum. Event destination type. - */ - type: EventDestination.Type; - - /** - * Time at which the object was last updated. - */ - updated: string; - - /** - * If using the v1 event namespace, the API version events are rendered as. - */ - v1_api_version: string | null; - - /** - * Webhook endpoint configuration. - */ - webhook_endpoint: EventDestination.WebhookEndpoint | null; - } - - namespace EventDestination { - interface AmazonEventbridge { - /** - * The AWS account ID. - */ - aws_account_id: string; - - /** - * The ARN of the AWS event source. - */ - aws_event_source_arn: string; - - /** - * Closed Enum. The state of the AWS event source. - */ - aws_event_source_status: AmazonEventbridge.AwsEventSourceStatus; - } - - namespace AmazonEventbridge { - type AwsEventSourceStatus = - | 'active' - | 'deleted' - | 'pending' - | 'unknown'; - } - - type EventNamespace = 'v1' | 'v2'; - - type EventsFrom = 'accounts' | 'self'; - - type Status = 'disabled' | 'enabled'; - - interface StatusDetails { - /** - * Details about why the event destination has been disabled. - */ - disabled: StatusDetails.Disabled | null; - } - - namespace StatusDetails { - interface Disabled { - /** - * Closed Enum. Reason event destination has been disabled. - */ - reason: Disabled.Reason; - } - - namespace Disabled { - type Reason = 'no_aws_event_source_exists' | 'user'; - } - } - - type Type = 'amazon_eventbridge' | 'webhook_endpoint'; - - interface WebhookEndpoint { - /** - * The URL of the webhook endpoint. - */ - url: string; - } - } - } - } -} diff --git a/types/V2/EventTypes.d.ts b/types/V2/EventTypes.d.ts index d8f54e86fe..a75f67a0ff 100644 --- a/types/V2/EventTypes.d.ts +++ b/types/V2/EventTypes.d.ts @@ -4,8 +4,7 @@ declare module 'stripe' { namespace Stripe.V2 { export type Event = | Stripe.Events.V1BillingMeterErrorReportTriggeredEvent - | Stripe.Events.V1BillingMeterNoMeterFoundEvent - | Stripe.Events.V2CoreEventDestinationPingEvent; + | Stripe.Events.V1BillingMeterNoMeterFoundEvent; } namespace Stripe.Events { @@ -211,14 +210,5 @@ declare module 'stripe' { } } } - - /** - * A ping event used to test the connection to an event destination. - */ - export interface V2CoreEventDestinationPingEvent extends V2.EventBase { - type: 'v2.core.event_destination.ping'; - // Retrieves the object associated with the event. - fetchRelatedObject(): Promise; - } } } diff --git a/types/V2/Events.d.ts b/types/V2/Events.d.ts index 5808e8a158..4a42091348 100644 --- a/types/V2/Events.d.ts +++ b/types/V2/Events.d.ts @@ -43,7 +43,7 @@ declare module 'stripe' { /** * String representing the object's type. Objects of the same type share the same value of the object field. */ - object: 'event'; + object: 'v2.core.event'; /** * Authentication context needed to fetch the event or related object. diff --git a/types/index.d.ts b/types/index.d.ts index 41f9bfc7e0..e56eaec38f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -273,7 +273,6 @@ /// /// /// -/// /// /// // Imports: The end of the section generated from our OpenAPI spec From 1bf4c2e3b886736de24e27c5f45f9ade5e186a76 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Wed, 25 Sep 2024 09:32:24 -0700 Subject: [PATCH 04/20] Pull latest proto, V2 suffix is now gone from MeterEvent & MeterEventAdjustment --- ...stmentV2S.d.ts => MeterEventAdjustments.d.ts} | 16 ++++++++-------- .../Billing/MeterEventAdjustmentsResource.d.ts | 2 +- types/V2/Billing/MeterEventStreamResource.d.ts | 2 +- .../{MeterEventV2S.d.ts => MeterEvents.d.ts} | 4 ++-- types/V2/Billing/MeterEventsResource.d.ts | 4 ++-- types/V2/Core/EventsResource.d.ts | 6 +++--- types/index.d.ts | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) rename types/V2/Billing/{MeterEventAdjustmentV2S.d.ts => MeterEventAdjustments.d.ts} (73%) rename types/V2/Billing/{MeterEventV2S.d.ts => MeterEvents.d.ts} (95%) diff --git a/types/V2/Billing/MeterEventAdjustmentV2S.d.ts b/types/V2/Billing/MeterEventAdjustments.d.ts similarity index 73% rename from types/V2/Billing/MeterEventAdjustmentV2S.d.ts rename to types/V2/Billing/MeterEventAdjustments.d.ts index de99807156..11b670631e 100644 --- a/types/V2/Billing/MeterEventAdjustmentV2S.d.ts +++ b/types/V2/Billing/MeterEventAdjustments.d.ts @@ -5,9 +5,9 @@ declare module 'stripe' { namespace V2 { namespace Billing { /** - * The MeterEventAdjustmentV2 object. + * The MeterEventAdjustment object. */ - interface MeterEventAdjustmentV2 { + interface MeterEventAdjustment { /** * The unique id of this meter event adjustment. */ @@ -16,12 +16,12 @@ declare module 'stripe' { /** * String representing the object's type. Objects of the same type share the same value of the object field. */ - object: 'v2.billing.meter_event_adjustment'; + object: 'billing.meter_event_adjustment'; /** * Specifies which event to cancel. */ - cancel: MeterEventAdjustmentV2.Cancel; + cancel: MeterEventAdjustment.Cancel; /** * The time the adjustment was created. @@ -39,17 +39,17 @@ declare module 'stripe' { livemode: boolean; /** - * The meter event adjustment's status. + * Open Enum. The meter event adjustment's status. */ - status: MeterEventAdjustmentV2.Status; + status: MeterEventAdjustment.Status; /** - * Specifies whether to cancel a single event or a range of events for a time period. Time period cancellation is not supported yet. + * Open Enum. Specifies whether to cancel a single event or a range of events for a time period. Time period cancellation is not supported yet. */ type: 'cancel'; } - namespace MeterEventAdjustmentV2 { + namespace MeterEventAdjustment { interface Cancel { /** * Unique identifier for the event. You can only cancel events within 24 hours of Stripe receiving them. diff --git a/types/V2/Billing/MeterEventAdjustmentsResource.d.ts b/types/V2/Billing/MeterEventAdjustmentsResource.d.ts index 696df61388..310d13b7a5 100644 --- a/types/V2/Billing/MeterEventAdjustmentsResource.d.ts +++ b/types/V2/Billing/MeterEventAdjustmentsResource.d.ts @@ -39,7 +39,7 @@ declare module 'stripe' { create( params: MeterEventAdjustmentCreateParams, options?: RequestOptions - ): Promise>; + ): Promise>; } } } diff --git a/types/V2/Billing/MeterEventStreamResource.d.ts b/types/V2/Billing/MeterEventStreamResource.d.ts index 56f8cf51f1..fc4e65e3d4 100644 --- a/types/V2/Billing/MeterEventStreamResource.d.ts +++ b/types/V2/Billing/MeterEventStreamResource.d.ts @@ -48,7 +48,7 @@ declare module 'stripe' { namespace Billing { class MeterEventStreamResource { /** - * Sends a meter event for asynchronous processing. Supports higher rate limits. Requires a meter event session for authentication. + * Creates meter events. Events are processed asynchronously, including validation. Requires a meter event session for authentication. Supports up to 10,000 requests per second in livemode. For even higher rate-limits, contact sales. * @throws Stripe.TemporarySessionExpiredError */ create( diff --git a/types/V2/Billing/MeterEventV2S.d.ts b/types/V2/Billing/MeterEvents.d.ts similarity index 95% rename from types/V2/Billing/MeterEventV2S.d.ts rename to types/V2/Billing/MeterEvents.d.ts index c7033817d4..53aac66a23 100644 --- a/types/V2/Billing/MeterEventV2S.d.ts +++ b/types/V2/Billing/MeterEvents.d.ts @@ -7,11 +7,11 @@ declare module 'stripe' { /** * Fix me empty_doc_string. */ - interface MeterEventV2 { + interface MeterEvent { /** * String representing the object's type. Objects of the same type share the same value of the object field. */ - object: 'v2.billing.meter_event'; + object: 'billing.meter_event'; /** * The creation time of this meter event. diff --git a/types/V2/Billing/MeterEventsResource.d.ts b/types/V2/Billing/MeterEventsResource.d.ts index db15e41043..00def8d19f 100644 --- a/types/V2/Billing/MeterEventsResource.d.ts +++ b/types/V2/Billing/MeterEventsResource.d.ts @@ -39,12 +39,12 @@ declare module 'stripe' { namespace Billing { class MeterEventsResource { /** - * Creates a meter event. Validates the event synchronously. + * Creates a meter event. Events are validated synchronously, but are processed asynchronously. Supports up to 1,000 events per second in livemode. For higher rate-limits, please use meter event streams instead. */ create( params: MeterEventCreateParams, options?: RequestOptions - ): Promise>; + ): Promise>; } } } diff --git a/types/V2/Core/EventsResource.d.ts b/types/V2/Core/EventsResource.d.ts index e74b6bee42..3fd2f2e3c3 100644 --- a/types/V2/Core/EventsResource.d.ts +++ b/types/V2/Core/EventsResource.d.ts @@ -29,11 +29,11 @@ declare module 'stripe' { id: string, params?: EventRetrieveParams, options?: RequestOptions - ): Promise>; + ): Promise>; retrieve( id: string, options?: RequestOptions - ): Promise>; + ): Promise>; /** * List events, going back up to 30 days. @@ -41,7 +41,7 @@ declare module 'stripe' { list( params: EventListParams, options?: RequestOptions - ): ApiListPromise; + ): V2.ApiListPromise; } } } diff --git a/types/index.d.ts b/types/index.d.ts index e56eaec38f..cfc295c805 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -270,9 +270,9 @@ /// /// /// -/// +/// /// -/// +/// /// /// // Imports: The end of the section generated from our OpenAPI spec From b4f78ec06fa704ecae68c06220b74696487afb45 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Wed, 25 Sep 2024 10:50:45 -0700 Subject: [PATCH 05/20] preview version not needed anymore --- src/RequestSender.ts | 6 +----- src/apiVersion.ts | 3 +-- test/RequestSender.spec.ts | 3 +-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/RequestSender.ts b/src/RequestSender.ts index 7c73a17270..2cc72e5058 100644 --- a/src/RequestSender.ts +++ b/src/RequestSender.ts @@ -22,7 +22,6 @@ import { StripeObject, StripeRequest, } from './Types.js'; -import {PreviewVersion} from './apiVersion.js'; import {HttpClient, HttpClientResponseInterface} from './net/HttpClient.js'; import { emitWarning, @@ -620,10 +619,7 @@ export class RequestSender { requestData = data; this._stripe.getClientUserAgent((clientUserAgent: string) => { - const apiVersion = - apiMode == 'v2' - ? PreviewVersion - : this._stripe.getApiField('version'); + const apiVersion = this._stripe.getApiField('version'); const headers = this._makeHeaders({ contentType: apiMode == 'v2' diff --git a/src/apiVersion.ts b/src/apiVersion.ts index a59577235b..c59d0bde0f 100644 --- a/src/apiVersion.ts +++ b/src/apiVersion.ts @@ -1,4 +1,3 @@ // File generated from our OpenAPI spec -export const ApiVersion = '2024-06-20'; -export const PreviewVersion = '2024-09-30.acacia'; +export const ApiVersion = '2024-09-30.acacia'; diff --git a/test/RequestSender.spec.ts b/test/RequestSender.spec.ts index 21bdd2ca6c..f69609afc0 100644 --- a/test/RequestSender.spec.ts +++ b/test/RequestSender.spec.ts @@ -3,7 +3,6 @@ import {expect} from 'chai'; import nock = require('nock'); import { - InsufficientFundsError, StripeAuthenticationError, StripeConnectionError, StripeError, @@ -14,7 +13,7 @@ import { TemporarySessionExpiredError, } from '../src/Error.js'; import {RequestSender} from '../src/RequestSender.js'; -import {ApiVersion, PreviewVersion} from '../src/apiVersion.js'; +import {ApiVersion} from '../src/apiVersion.js'; import {HttpClientResponse} from '../src/net/HttpClient.js'; import { FAKE_API_KEY, From f79280d98750de1452a3d57daab011b1ec878aad Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Wed, 25 Sep 2024 10:50:53 -0700 Subject: [PATCH 06/20] Fix namespace issue in Event class --- types/V2/Core/EventsResource.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/V2/Core/EventsResource.d.ts b/types/V2/Core/EventsResource.d.ts index 3fd2f2e3c3..e74b6bee42 100644 --- a/types/V2/Core/EventsResource.d.ts +++ b/types/V2/Core/EventsResource.d.ts @@ -29,11 +29,11 @@ declare module 'stripe' { id: string, params?: EventRetrieveParams, options?: RequestOptions - ): Promise>; + ): Promise>; retrieve( id: string, options?: RequestOptions - ): Promise>; + ): Promise>; /** * List events, going back up to 30 days. @@ -41,7 +41,7 @@ declare module 'stripe' { list( params: EventListParams, options?: RequestOptions - ): V2.ApiListPromise; + ): ApiListPromise; } } } From e1b356f7664630d169b28c05019a456ed3411401 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Wed, 25 Sep 2024 11:29:56 -0700 Subject: [PATCH 07/20] added snippets folder with a project and instructions for getting started to contain self contained examples --- examples/snippets/README.md | 25 +++ examples/snippets/meter_event_stream.ts | 39 ++++ examples/snippets/new_example.ts | 7 + examples/snippets/package.json | 12 ++ examples/snippets/yarn.lock | 245 ++++++++++++++++++++++++ 5 files changed, 328 insertions(+) create mode 100644 examples/snippets/README.md create mode 100644 examples/snippets/meter_event_stream.ts create mode 100644 examples/snippets/new_example.ts create mode 100644 examples/snippets/package.json create mode 100644 examples/snippets/yarn.lock diff --git a/examples/snippets/README.md b/examples/snippets/README.md new file mode 100644 index 0000000000..ce80f0454d --- /dev/null +++ b/examples/snippets/README.md @@ -0,0 +1,25 @@ +## Setup + +1. From the project root, run `yarn build` to build the modules. +2. Run `yarn` to install node dependencies. This will reference the local Stripe SDK modules created in step 1. + +If on step 2 you see an error `Error: unsure how to copy this: /Users/jar/stripe/sdks/node/.git/fsmonitor--daemon.ipc`: +run `rm /path/to/node/sdk/.git/fsmonitor--daemon.ipc && yarn` +This file is used by a file monitor built into git. Removing it temporarily does not seem to affect its operation, and this one liner will let `yarn` succeed. + +## Running an example + +If your example is in typescript, run: +`yarn run ts-node your_example.ts` + +If your example is in javascript, run: +`node your_example.js` +or +`node your_example.mjs` + +## Adding a new example + +1. Clone new_example.ts +2. Implement your example +3. Run it (as per above) +4. 👍 diff --git a/examples/snippets/meter_event_stream.ts b/examples/snippets/meter_event_stream.ts new file mode 100644 index 0000000000..6039ede351 --- /dev/null +++ b/examples/snippets/meter_event_stream.ts @@ -0,0 +1,39 @@ +import {Stripe} from 'stripe'; + +const apiKey = '{{API_KEY}}'; +const customerId = '{{CUSTOMER_ID}}'; + +let meterEventSession: null | any = null; + +async function refreshMeterEventSession() { + if ( + meterEventSession === null || + new Date(meterEventSession.expires_at * 1000) <= new Date() + ) { + // Create a new meter event session in case the existing session expired + const client = new Stripe(apiKey); + meterEventSession = await client.v2.billing.meterEventSession.create(); + } +} + +async function sendMeterEvent(meterEvent: any) { + // Refresh the meter event session, if necessary + await refreshMeterEventSession(); + + // Create a meter event + const client = new Stripe(meterEventSession.authentication_token); + await client.v2.billing.meterEventStream.create({ + events: [meterEvent], + }); +} + +// Send meter events +sendMeterEvent({ + event_name: 'alpaca_ai_tokens', + payload: { + stripe_customer_id: customerId, // Replace with actual customer ID + value: '27', + }, +}).catch((error) => { + console.error('Error sending meter event:', error); +}); diff --git a/examples/snippets/new_example.ts b/examples/snippets/new_example.ts new file mode 100644 index 0000000000..e521280c83 --- /dev/null +++ b/examples/snippets/new_example.ts @@ -0,0 +1,7 @@ +import {Stripe} from 'stripe'; + +const apiKey = '{{API_KEY}}'; + +console.log('Hello World'); +// const client = new Stripe(apiKey); +// client.v2.... diff --git a/examples/snippets/package.json b/examples/snippets/package.json new file mode 100644 index 0000000000..e5acf2cc64 --- /dev/null +++ b/examples/snippets/package.json @@ -0,0 +1,12 @@ +{ + "name": "snippets", + "version": "1.0.0", + "description": "example Stripe SDK code snippets", + "main": "index.js", + "license": "ISC", + "dependencies": { + "stripe": "file:../../", + "ts-node": "^10.9.2", + "typescript": "^5.6.2" + } +} diff --git a/examples/snippets/yarn.lock b/examples/snippets/yarn.lock new file mode 100644 index 0000000000..a53c23f068 --- /dev/null +++ b/examples/snippets/yarn.lock @@ -0,0 +1,245 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/node@>=8.1.0": + version "22.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.6.1.tgz#e531a45f4d78f14a8468cb9cdc29dc9602afc7ac" + integrity sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw== + dependencies: + undici-types "~6.19.2" + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +qs@^6.11.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +"stripe@file:../..": + version "16.12.0" + dependencies: + "@types/node" ">=8.1.0" + qs "^6.11.0" + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +typescript@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 1a6573a7e3e95b267816d350c1184dd79509c49d Mon Sep 17 00:00:00 2001 From: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:15:41 -0700 Subject: [PATCH 08/20] Update sdk version --- src/stripe.core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stripe.core.ts b/src/stripe.core.ts index d0e59b14df..635987fdaf 100644 --- a/src/stripe.core.ts +++ b/src/stripe.core.ts @@ -59,7 +59,7 @@ export function createStripe( platformFunctions: PlatformFunctions, requestSender: RequestSenderFactory = defaultRequestSenderFactory ): typeof Stripe { - Stripe.PACKAGE_VERSION = '0.56.0'; + Stripe.PACKAGE_VERSION = '16.12.0'; Stripe.USER_AGENT = { bindings_version: Stripe.PACKAGE_VERSION, lang: 'node', From 21e6577c9276ea6dfc693f5e1c011a712651d7fc Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Wed, 25 Sep 2024 18:53:53 -0700 Subject: [PATCH 09/20] added stripe_webhook_handler example added express to example dependencies --- examples/snippets/package.json | 1 + examples/snippets/stripe_webhook_handler.js | 35 ++ examples/snippets/yarn.lock | 354 +++++++++++++++++++- 3 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/stripe_webhook_handler.js diff --git a/examples/snippets/package.json b/examples/snippets/package.json index e5acf2cc64..ba3738f172 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -5,6 +5,7 @@ "main": "index.js", "license": "ISC", "dependencies": { + "express": "^4.21.0", "stripe": "file:../../", "ts-node": "^10.9.2", "typescript": "^5.6.2" diff --git a/examples/snippets/stripe_webhook_handler.js b/examples/snippets/stripe_webhook_handler.js new file mode 100644 index 0000000000..90d77a8e98 --- /dev/null +++ b/examples/snippets/stripe_webhook_handler.js @@ -0,0 +1,35 @@ +const express = require('express'); +const {Stripe} = require('stripe'); + +const app = express(); + +const apiKey = process.env.STRIPE_API_KEY; +const webhookSecret = process.env.WEBHOOK_SECRET; + +const client = new Stripe(); +app.post( + '/webhook', + express.raw({type: 'application/json'}), + async (req, res) => { + const sig = req.headers['stripe-signature']; + + try { + const thinEvent = client.parseThinEvent(req.body, sig, webhookSecret); + + // Fetch the event data to understand the failure + const event = await client.v2.core.events.retrieve(thinEvent.id); + if (event.type == 'v1.billing.meter.error_report_triggered') { + const meter = await event.fetchRelatedObject(); + const meterId = meter.id; + // Record the failures and alert your team + // Add your logic here + } + res.sendStatus(200); + } catch (err) { + console.log(`Webhook Error: ${err.message}`); + res.status(400).send(`Webhook Error: ${err.message}`); + } + } +); + +app.listen(4242, () => console.log('Running on port 4242')); diff --git a/examples/snippets/yarn.lock b/examples/snippets/yarn.lock index a53c23f068..622721e44e 100644 --- a/examples/snippets/yarn.lock +++ b/examples/snippets/yarn.lock @@ -54,6 +54,14 @@ dependencies: undici-types "~6.19.2" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -71,6 +79,34 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -82,11 +118,40 @@ call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -96,11 +161,36 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -113,6 +203,76 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.21.0: + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" @@ -160,23 +320,177 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + object-inspect@^1.13.1: version "1.13.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== -qs@^6.11.0: +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.13.0, qs@^6.11.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -189,6 +503,11 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -199,12 +518,22 @@ side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "stripe@file:../..": version "16.12.0" dependencies: "@types/node" ">=8.1.0" qs "^6.11.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + ts-node@^10.9.2: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" @@ -224,6 +553,14 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typescript@^5.6.2: version "5.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" @@ -234,11 +571,26 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" From c7ad0c320f9487409177737dced2f890c1cb531f Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Wed, 25 Sep 2024 18:55:12 -0700 Subject: [PATCH 10/20] added apiKey to Stripe constructor --- examples/snippets/stripe_webhook_handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/snippets/stripe_webhook_handler.js b/examples/snippets/stripe_webhook_handler.js index 90d77a8e98..28fcb9ed85 100644 --- a/examples/snippets/stripe_webhook_handler.js +++ b/examples/snippets/stripe_webhook_handler.js @@ -6,7 +6,8 @@ const app = express(); const apiKey = process.env.STRIPE_API_KEY; const webhookSecret = process.env.WEBHOOK_SECRET; -const client = new Stripe(); +const client = new Stripe(apiKey); + app.post( '/webhook', express.raw({type: 'application/json'}), From 9ab1416c5c0b2db8dad8c7286d47446f7da646f8 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Thu, 26 Sep 2024 00:59:43 -0700 Subject: [PATCH 11/20] Remove unused request authenticator --- src/stripe.core.ts | 106 --------------------------------------------- 1 file changed, 106 deletions(-) diff --git a/src/stripe.core.ts b/src/stripe.core.ts index 635987fdaf..c6a930ff17 100644 --- a/src/stripe.core.ts +++ b/src/stripe.core.ts @@ -4,9 +4,7 @@ import {StripeResource} from './StripeResource.js'; import { AppInfo, RequestAuthenticator, - RequestHeaders, StripeObject, - StripeRequest, UserProvidedConfig, } from './Types.js'; import {WebhookObject, WebhookEvent, createWebhooks} from './Webhooks.js'; @@ -201,110 +199,6 @@ export function createStripe( Stripe.createSubtleCryptoProvider = platformFunctions.createSubtleCryptoProvider; - Stripe.createRequestSigningAuthenticator = ( - keyId: string, - sign: (signatureBase: Uint8Array) => Promise, - crypto: CryptoProvider = Stripe.createNodeCryptoProvider() - ): RequestAuthenticator => { - const authorizationHeaderName = 'Authorization'; - const stripeContextHeaderName = 'Stripe-Context'; - const stripeAccountHeaderName = 'Stripe-Account'; - const contentDigestHeaderName = 'Content-Digest'; - const signatureInputHeaderName = 'Signature-Input'; - const signatureHeaderName = 'Signature'; - const coveredHeaderNames = [ - 'Content-Type', - contentDigestHeaderName, - stripeContextHeaderName, - stripeAccountHeaderName, - authorizationHeaderName, - ].map((h) => h.toLowerCase()); - - const coveredHeaderNamesGet = [ - stripeContextHeaderName, - stripeAccountHeaderName, - authorizationHeaderName, - ].map((h) => h.toLowerCase()); - - const formatCoveredHeaders = (headers: Array): string => { - return `(${headers.map((h) => `"${h}"`).join(' ')})`; - }; - const coveredHeaderFormatted = formatCoveredHeaders(coveredHeaderNames); - const coveredHeaderGetFormatted = formatCoveredHeaders( - coveredHeaderNamesGet - ); - - const getSignatureInput = (method: string, created: number): string => { - const coveredHeaderNames = - method == 'GET' ? coveredHeaderGetFormatted : coveredHeaderFormatted; - return `${coveredHeaderNames};created=${created}`; - }; - - const encoder = new TextEncoder(); - - const initializedCrypto = crypto ?? Stripe.createNodeCryptoProvider(); - - const calculateDigestHeader = async (content: string): Promise => { - const digest = await initializedCrypto.computeSHA256Async( - encoder.encode(content) - ); - return `sha-256=:${Buffer.from(digest).toString('base64')}:`; - }; - - const calculateSignatureBase = ( - request: StripeRequest, - created: number - ): string => { - const stringifyHeaderValues = ( - value: string | number | string[] | null - ): string => { - if (value == null) { - return ''; - } - return (value instanceof Array ? value : [value]).join(','); - }; - - const headerNames = - request.method == 'GET' ? coveredHeaderNamesGet : coveredHeaderNames; - const lowercaseHeaders: RequestHeaders = {}; - const keys = Object.keys(request.headers); - keys.forEach((k) => { - lowercaseHeaders[k.toLowerCase()] = request.headers[k]; - }); - - const inputs = headerNames - .map( - (header) => - `"${header}": ${stringifyHeaderValues(lowercaseHeaders[header])}` - ) - .join('\n'); - - const signatureInput = getSignatureInput(request.method, created); - - return `${inputs}\n"@signature-params": ${signatureInput}`; - }; - - return async (request): Promise => { - if (request.method != 'GET') { - request.headers[contentDigestHeaderName] = await calculateDigestHeader( - request.body ?? '' - ); - } - - const created = Math.floor(Date.now() / 1000); - - request.headers[authorizationHeaderName] = 'STRIPE-V2-SIG ' + keyId; - request.headers[signatureInputHeaderName] = - 'sig1=' + getSignatureInput(request.method, created); - - const signatureBase = calculateSignatureBase(request, created); - const signature = await sign(encoder.encode(signatureBase)); - - request.headers[signatureHeaderName] = - 'sig1=:' + Buffer.from(signature).toString('base64') + ':'; - }; - }; - Stripe.prototype = { // Properties are set in the constructor above _appInfo: undefined!, From 31da0da958eceed269d851ac7176230b2a1c9adf Mon Sep 17 00:00:00 2001 From: prathmesh-stripe <165320323+prathmesh-stripe@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:58:16 -0400 Subject: [PATCH 12/20] Add raw request (#2189) * Raw request in node * lint fix * added a test --- README.md | 34 ++++++ src/RequestSender.ts | 98 ++++++++++++++-- src/Types.d.ts | 12 ++ src/stripe.core.ts | 11 ++ src/utils.ts | 16 ++- test/stripe.spec.ts | 209 +++++++++++++++++++++++++++++++++++ types/index.d.ts | 19 ++++ types/lib.d.ts | 9 +- types/test/typescriptTest.ts | 6 +- 9 files changed, 396 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5adfcd7f5a..2300d84ba8 100644 --- a/README.md +++ b/README.md @@ -517,6 +517,40 @@ const stripe = new Stripe('sk_test_...', { }); ``` +### Custom requests + +If you would like to send a request to an undocumented API (for example you are in a private beta), or if you prefer to bypass the method definitions in the library and specify your request details directly, you can use the `rawRequest` method on the StripeClient object. + +```javascript +const client = new Stripe('sk_test_...'); + +client.rawRequest( + 'POST', + '/v1/beta_endpoint', + { param: 123 }, + { apiVersion: '2022-11-15; feature_beta=v3' } + ) + .then((response) => /* handle response */ ) + .catch((error) => console.error(error)); +``` + +Or using ES modules and `async`/`await`: + +```javascript +import Stripe from 'stripe'; +const stripe = new Stripe('sk_test_...'); + +const response = await stripe.rawRequest( + 'POST', + '/v1/beta_endpoint', + { param: 123 }, + { apiVersion: '2022-11-15; feature_beta=v3' } +); + +// handle response +``` + + ## Support New features and bug fixes are released on the latest major version of the `stripe` package. If you are on an older major version, we recommend that you upgrade to the latest in order to use the new features and bug fixes including those for security vulnerabilities. Older major versions of the package will continue to be available for use, but will not be receiving any updates. diff --git a/src/RequestSender.ts b/src/RequestSender.ts index 2cc72e5058..06b13d3717 100644 --- a/src/RequestSender.ts +++ b/src/RequestSender.ts @@ -8,19 +8,21 @@ import { StripeRateLimitError, } from './Error.js'; import { - ApiMode, - RequestAuthenticator, + StripeObject, + RequestHeaders, + RequestEvent, + ResponseEvent, RequestCallback, RequestCallbackReturn, RequestData, RequestDataProcessor, - RequestEvent, - RequestHeaders, RequestOptions, RequestSettings, - ResponseEvent, - StripeObject, StripeRequest, + RequestOpts, + RequestArgs, + RequestAuthenticator, + ApiMode, } from './Types.js'; import {HttpClient, HttpClientResponseInterface} from './net/HttpClient.js'; import { @@ -30,6 +32,8 @@ import { queryStringifyRequestData, removeNullish, getAPIMode, + getOptionsFromArgs, + getDataFromArgs, } from './utils.js'; export type HttpClientResponseError = {code: string}; @@ -457,11 +461,87 @@ export class RequestSender { } } + _rawRequest( + method: string, + path: string, + params?: RequestData, + options?: RequestOptions + ): Promise { + const requestPromise = new Promise((resolve, reject) => { + let opts: RequestOpts; + try { + const requestMethod = method.toUpperCase(); + if ( + requestMethod !== 'POST' && + params && + Object.keys(params).length !== 0 + ) { + throw new Error( + 'rawRequest only supports params on POST requests. Please pass null and add your parameters to path.' + ); + } + const args: RequestArgs = [].slice.call([params, options]); + + // Pull request data and options (headers, auth) from args. + const dataFromArgs = getDataFromArgs(args); + const data = Object.assign({}, dataFromArgs); + const calculatedOptions = getOptionsFromArgs(args); + + const headers = calculatedOptions.headers; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const authenticator: RequestAuthenticator = calculatedOptions.authenticator!; + opts = { + requestMethod, + requestPath: path, + bodyData: data, + queryData: {}, + authenticator, + headers, + host: null, + streaming: false, + settings: {}, + usage: ['raw_request'], + }; + } catch (err) { + reject(err); + return; + } + + function requestCallback( + err: any, + response: HttpClientResponseInterface + ): void { + if (err) { + reject(err); + } else { + resolve(response); + } + } + + const {headers, settings} = opts; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const authenticator: RequestAuthenticator = opts.authenticator!; + + this._request( + opts.requestMethod, + opts.host, + path, + opts.bodyData, + authenticator, + {headers, settings, streaming: opts.streaming}, + opts.usage, + requestCallback + ); + }); + + return requestPromise; + } + _request( method: string, host: string | null, path: string, - data: RequestData, + data: RequestData | null, authenticator: RequestAuthenticator, options: RequestOptions, usage: Array = [], @@ -469,7 +549,7 @@ export class RequestSender { requestDataProcessor: RequestDataProcessor | null = null ): void { let requestData: string; - authenticator = authenticator ?? this._stripe._authenticator; + authenticator = authenticator ?? this._stripe._authenticator ?? null; const apiMode: ApiMode = getAPIMode(path); const retryRequest = ( requestFn: typeof makeRequest, @@ -603,7 +683,7 @@ export class RequestSender { } }); }) - .catch((e) => { + .catch((e: any) => { throw new StripeError({ message: 'Unable to authenticate the request', exception: e, diff --git a/src/Types.d.ts b/src/Types.d.ts index 8b94aa6d9f..735a4f7b07 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -185,8 +185,20 @@ export type StripeObject = { key: string, authenticator: RequestAuthenticator | undefined ) => void; + rawRequest: ( + method: string, + path: string, + data: RequestData, + options: RequestOptions + ) => Promise; }; export type RequestSender = { + _rawRequest( + method: string, + path: string, + params?: RequestData, + options?: RequestOptions + ): Promise; _request( method: string, host: string | null, diff --git a/src/stripe.core.ts b/src/stripe.core.ts index c6a930ff17..77e9f3cc17 100644 --- a/src/stripe.core.ts +++ b/src/stripe.core.ts @@ -6,6 +6,8 @@ import { RequestAuthenticator, StripeObject, UserProvidedConfig, + RequestData, + RequestOptions, } from './Types.js'; import {WebhookObject, WebhookEvent, createWebhooks} from './Webhooks.js'; import {ApiVersion} from './apiVersion.js'; @@ -216,6 +218,15 @@ export function createStripe( _requestSender: null!, _platformFunctions: null!, + rawRequest( + method: string, + path: string, + params?: RequestData, + options?: RequestOptions + ): Promise { + return this._requestSender._rawRequest(method, path, params, options); + }, + /** * @private */ diff --git a/src/utils.ts b/src/utils.ts index 4ad04bf64e..a9dfe7b143 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,7 @@ const OPTIONS_KEYS = [ 'host', 'authenticator', 'stripeContext', + 'additionalHeaders', ]; type Settings = { @@ -33,7 +34,7 @@ type Options = { host: string | null; settings: Settings; streaming?: boolean; - headers: Record; + headers: RequestHeaders; }; export function isOptionsHash(o: unknown): boolean | unknown { @@ -166,10 +167,10 @@ export function getOptionsFromArgs(args: RequestArgs): Options { opts.authenticator = createApiKeyAuthenticator(params.apiKey as string); } if (params.idempotencyKey) { - opts.headers['Idempotency-Key'] = params.idempotencyKey; + opts.headers['Idempotency-Key'] = params.idempotencyKey as string; } if (params.stripeAccount) { - opts.headers['Stripe-Account'] = params.stripeAccount; + opts.headers['Stripe-Account'] = params.stripeAccount as string; } if (params.stripeContext) { if (opts.headers['Stripe-Account']) { @@ -177,10 +178,10 @@ export function getOptionsFromArgs(args: RequestArgs): Options { "Can't specify both stripeAccount and stripeContext." ); } - opts.headers['Stripe-Context'] = params.stripeContext; + opts.headers['Stripe-Context'] = params.stripeContext as string; } if (params.apiVersion) { - opts.headers['Stripe-Version'] = params.apiVersion; + opts.headers['Stripe-Version'] = params.apiVersion as string; } if (Number.isInteger(params.maxNetworkRetries)) { opts.settings.maxNetworkRetries = params.maxNetworkRetries as number; @@ -203,6 +204,11 @@ export function getOptionsFromArgs(args: RequestArgs): Options { } opts.authenticator = params.authenticator as RequestAuthenticator; } + if (params.additionalHeaders) { + opts.headers = params.additionalHeaders as { + [headerName: string]: string; + }; + } } } return opts; diff --git a/test/stripe.spec.ts b/test/stripe.spec.ts index 91e744079c..b401b41890 100644 --- a/test/stripe.spec.ts +++ b/test/stripe.spec.ts @@ -865,4 +865,213 @@ describe('Stripe Module', function() { }).to.throw(StripeSignatureVerificationError); }); }); + + describe('rawRequest', () => { + const returnedCustomer = { + id: 'cus_123', + }; + + it('should make request with specified encoding FORM', (done) => { + return getTestServerStripe( + {}, + (req, res) => { + expect(req.headers['content-type']).to.equal( + 'application/x-www-form-urlencoded' + ); + expect(req.headers['stripe-version']).to.equal(ApiVersion); + const requestBody = []; + req.on('data', (chunks) => { + requestBody.push(chunks); + }); + req.on('end', () => { + const body = Buffer.concat(requestBody).toString(); + expect(body).to.equal('description=test%20customer'); + }); + res.write(JSON.stringify(returnedCustomer)); + res.end(); + }, + async (err, stripe, closeServer) => { + if (err) return done(err); + try { + const result = await stripe.rawRequest( + 'POST', + '/v1/customers', + {description: 'test customer'}, + {} + ); + expect(result).to.deep.equal(returnedCustomer); + closeServer(); + done(); + } catch (err) { + return done(err); + } + } + ); + }); + + it('should make request with specified encoding JSON', (done) => { + return getTestServerStripe( + {}, + (req, res) => { + expect(req.headers['content-type']).to.equal('application/json'); + expect(req.headers['stripe-version']).to.equal(ApiVersion); + expect(req.headers.foo).to.equal('bar'); + const requestBody = []; + req.on('data', (chunks) => { + requestBody.push(chunks); + }); + req.on('end', () => { + const body = Buffer.concat(requestBody).toString(); + expect(body).to.equal( + '{"description":"test meter event","created":"1234567890"}' + ); + }); + res.write(JSON.stringify(returnedCustomer)); + res.end(); + }, + async (err, stripe, closeServer) => { + if (err) return done(err); + try { + const result = await stripe.rawRequest( + 'POST', + '/v2/billing/meter_events', + { + description: 'test meter event', + created: new Date('2009-02-13T23:31:30Z'), + }, + {additionalHeaders: {foo: 'bar'}} + ); + expect(result).to.deep.equal(returnedCustomer); + closeServer(); + done(); + } catch (err) { + return done(err); + } + } + ); + }); + + it('defaults to form encoding request if not specified', (done) => { + return getTestServerStripe( + {}, + (req, res) => { + expect(req.headers['content-type']).to.equal( + 'application/x-www-form-urlencoded' + ); + const requestBody = []; + req.on('data', (chunks) => { + requestBody.push(chunks); + }); + req.on('end', () => { + const body = Buffer.concat(requestBody).toString(); + expect(body).to.equal( + 'description=test%20customer&created=1234567890' + ); + }); + res.write(JSON.stringify(returnedCustomer)); + res.end(); + }, + async (err, stripe, closeServer) => { + if (err) return done(err); + try { + const result = await stripe.rawRequest('POST', '/v1/customers', { + description: 'test customer', + created: new Date('2009-02-13T23:31:30Z'), + }); + expect(result).to.deep.equal(returnedCustomer); + closeServer(); + done(); + } catch (err) { + return done(err); + } + } + ); + }); + + it('should make request with specified additional headers', (done) => { + return getTestServerStripe( + {}, + (req, res) => { + console.log(req.headers); + expect(req.headers.foo).to.equal('bar'); + res.write(JSON.stringify(returnedCustomer)); + res.end(); + }, + async (err, stripe, closeServer) => { + if (err) return done(err); + try { + const result = await stripe.rawRequest( + 'GET', + '/v1/customers/cus_123', + {}, + {additionalHeaders: {foo: 'bar'}} + ); + expect(result).to.deep.equal(returnedCustomer); + closeServer(); + done(); + } catch (err) { + return done(err); + } + } + ); + }); + + it('should make request successfully', async () => { + const response = await stripe.rawRequest('GET', '/v1/customers', {}); + + expect(response).to.have.property('object', 'list'); + }); + + it("should include 'raw_request' in usage telemetry", (done) => { + let telemetryHeader; + let shouldStayOpen = true; + return getTestServerStripe( + {}, + (req, res) => { + telemetryHeader = req.headers['x-stripe-client-telemetry']; + res.setHeader('Request-Id', `req_1`); + res.writeHeader(200); + res.write('{}'); + res.end(); + const ret = {shouldStayOpen}; + shouldStayOpen = false; + return ret; + }, + async (err, stripe, closeServer) => { + if (err) return done(err); + try { + await stripe.rawRequest( + 'POST', + '/v1/customers', + {description: 'test customer'}, + {} + ); + expect(telemetryHeader).to.equal(undefined); + await stripe.rawRequest( + 'POST', + '/v1/customers', + {description: 'test customer'}, + {} + ); + expect( + JSON.parse(telemetryHeader).last_request_metrics.usage + ).to.deep.equal(['raw_request']); + closeServer(); + done(); + } catch (err) { + return done(err); + } + } + ); + }); + + it('should throw error when passing in params to non-POST request', async () => { + await expect( + stripe.rawRequest('GET', '/v1/customers/cus_123', {foo: 'bar'}) + ).to.be.rejectedWith( + Error, + /rawRequest only supports params on POST requests. Please pass null and add your parameters to path./ + ); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index cfc295c805..1b35492e52 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -472,6 +472,25 @@ declare module 'stripe' { event: 'response', handler: (event: Stripe.ResponseEvent) => void ): void; + + /** + * Allows for sending "raw" requests to the Stripe API, which can be used for + * testing new API endpoints or performing requests that the library does + * not support yet. + * + * This is an experimental interface and is not yet stable. + * + * @param method - HTTP request method, 'GET', 'POST', or 'DELETE' + * @param path - The path of the request, e.g. '/v1/beta_endpoint' + * @param params - The parameters to include in the request body. + * @param options - Additional request options. + */ + rawRequest( + method: string, + path: string, + params?: {[key: string]: unknown}, + options?: Stripe.RawRequestOptions + ): Promise>; } export default Stripe; diff --git a/types/lib.d.ts b/types/lib.d.ts index d82037bbf7..66a33f2dc6 100644 --- a/types/lib.d.ts +++ b/types/lib.d.ts @@ -27,7 +27,7 @@ declare module 'stripe' { }): (...args: any[]) => Response; //eslint-disable-line @typescript-eslint/no-explicit-any static MAX_BUFFERED_REQUEST_METRICS: number; } - export type LatestApiVersion = '2024-06-20'; + export type LatestApiVersion = '2024-09-30.acacia'; export type HttpAgent = Agent; export type HttpProtocol = 'http' | 'https'; @@ -153,6 +153,13 @@ declare module 'stripe' { host?: string; } + export type RawRequestOptions = RequestOptions & { + /** + * Specify additional request headers. This is an experimental interface and is not yet stable. + */ + additionalHeaders?: {[headerName: string]: string}; + }; + export type Response = T & { lastResponse: { headers: {[key: string]: string}; diff --git a/types/test/typescriptTest.ts b/types/test/typescriptTest.ts index 09a0c3e58f..e83aaa73c3 100644 --- a/types/test/typescriptTest.ts +++ b/types/test/typescriptTest.ts @@ -9,7 +9,7 @@ import Stripe from 'stripe'; let stripe = new Stripe('sk_test_123', { - apiVersion: '2024-06-20', + apiVersion: '2024-09-30.acacia', }); stripe = new Stripe('sk_test_123'); @@ -26,7 +26,7 @@ stripe = new Stripe('sk_test_123', { // Check config object. stripe = new Stripe('sk_test_123', { - apiVersion: '2024-06-20', + apiVersion: '2024-09-30.acacia', typescript: true, maxNetworkRetries: 1, timeout: 1000, @@ -44,7 +44,7 @@ stripe = new Stripe('sk_test_123', { description: 'test', }; const opts: Stripe.RequestOptions = { - apiVersion: '2024-06-20', + apiVersion: '2024-09-30.acacia', }; const customer: Stripe.Customer = await stripe.customers.create(params, opts); From 9c07835e0b91e0ba811b1023a02dfc9ea1d6ec1f Mon Sep 17 00:00:00 2001 From: Helen Ye Date: Thu, 26 Sep 2024 09:13:01 -0400 Subject: [PATCH 13/20] print stack on failure in example --- examples/snippets/stripe_webhook_handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snippets/stripe_webhook_handler.js b/examples/snippets/stripe_webhook_handler.js index 28fcb9ed85..d4549a6d87 100644 --- a/examples/snippets/stripe_webhook_handler.js +++ b/examples/snippets/stripe_webhook_handler.js @@ -27,7 +27,7 @@ app.post( } res.sendStatus(200); } catch (err) { - console.log(`Webhook Error: ${err.message}`); + console.log(`Webhook Error: ${err.stack}`); res.status(400).send(`Webhook Error: ${err.message}`); } } From 4db10acf0e78e6818ea88b1456db76cc7f9844f3 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Thu, 26 Sep 2024 11:40:12 -0700 Subject: [PATCH 14/20] Remove tests for unused request auth --- test/stripe.spec.ts | 120 -------------------------------------------- 1 file changed, 120 deletions(-) diff --git a/test/stripe.spec.ts b/test/stripe.spec.ts index b401b41890..9f2503621c 100644 --- a/test/stripe.spec.ts +++ b/test/stripe.spec.ts @@ -723,126 +723,6 @@ describe('Stripe Module', function() { }); }); - describe('createRequestSigningAuthenticator', () => { - let oldDate; - beforeEach(() => { - oldDate = Date.now; - Date.now = (): number => 123456789000; - }); - - afterEach(() => { - Date.now = oldDate; - }); - - it('authenticator applies signature for POST requests', async () => { - const signatureBases = []; - const authenticator = Stripe.createRequestSigningAuthenticator( - 'keyid', - (signatureBase) => { - signatureBases.push(signatureBase); - return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); - } - ); - - const request = { - method: 'POST', - body: '{"string":"String!"}', - headers: {'Content-Type': 'application/json'}, - }; - - await authenticator(request); - - expect(new TextDecoder().decode(signatureBases[0])).to.equal( - '"content-type": application/json\n' + - '"content-digest": sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:\n' + - '"stripe-context": \n' + - '"stripe-account": \n' + - '"authorization": STRIPE-V2-SIG keyid\n' + - '"@signature-params": ("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");created=123456789' - ); - expect(request.headers['Signature-Input']).to.equal( - 'sig1=("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");' + - 'created=123456789' - ); - expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); - expect(request.headers['Content-Digest']).to.equal( - 'sha-256=:HA3i38j+04ac71IzPtG1JK8o4q9sPK0fYPmJHmci5bg=:' - ); - expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); - expect(request.headers['Content-Type']).to.equal('application/json'); - }); - - it(`authenticator applies signature for DELETE requests`, async () => { - const signatureBases = []; - const authenticator = Stripe.createRequestSigningAuthenticator( - 'keyid', - (signatureBase) => { - signatureBases.push(signatureBase); - return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); - } - ); - - const request = { - method: 'DELETE', - body: null, - headers: {'Content-Type': 'application/json'}, - }; - - await authenticator(request); - - expect(new TextDecoder().decode(signatureBases[0])).to.equal( - '"content-type": application/json\n' + - '"content-digest": sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:\n' + - '"stripe-context": \n' + - '"stripe-account": \n' + - '"authorization": STRIPE-V2-SIG keyid\n' + - '"@signature-params": ("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");created=123456789' - ); - expect(request.headers['Signature-Input']).to.equal( - 'sig1=("content-type" "content-digest" "stripe-context" "stripe-account" "authorization");' + - 'created=123456789' - ); - expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); - expect(request.headers['Content-Digest']).to.equal( - 'sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:' - ); - expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); - expect(request.headers['Content-Type']).to.equal('application/json'); - }); - - it('authenticator applies signature for GET requests', async () => { - const signatureBases = []; - const authenticator = Stripe.createRequestSigningAuthenticator( - 'keyid', - (signatureBase) => { - signatureBases.push(signatureBase); - return Promise.resolve(new Uint8Array([1, 2, 3, 4, 5])); - } - ); - - const request = { - method: 'GET', - headers: {}, - }; - - await authenticator(request); - - expect(new TextDecoder().decode(signatureBases[0])).to.equal( - '"stripe-context": \n' + - '"stripe-account": \n' + - '"authorization": STRIPE-V2-SIG keyid\n' + - '"@signature-params": ("stripe-context" "stripe-account" "authorization");created=123456789' - ); - expect(request.headers['Signature-Input']).to.equal( - 'sig1=("stripe-context" "stripe-account" "authorization");' + - 'created=123456789' - ); - expect(request.headers.Signature).to.equal('sig1=:AQIDBAU=:'); - expect(request.headers['Content-Digest']).to.equal(undefined); - expect(request.headers.Authorization).to.equal('STRIPE-V2-SIG keyid'); - }); - }); - describe('parseThinEvent', () => { const secret = 'whsec_test_secret'; From e1eb3168d254a3b0e09778a90f653ce0092af544 Mon Sep 17 00:00:00 2001 From: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:02:08 -0700 Subject: [PATCH 15/20] Edit test names --- test/RequestSender.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/RequestSender.spec.ts b/test/RequestSender.spec.ts index f69609afc0..6927803959 100644 --- a/test/RequestSender.spec.ts +++ b/test/RequestSender.spec.ts @@ -479,7 +479,7 @@ describe('RequestSender', () => { ); }); - it('sends current v1 version when apiMode is v1', (done) => { + it('sends with APIVersion in header', (done) => { const host = stripe.getConstant('DEFAULT_HOST'); const scope = nock(`https://${host}`, { reqheaders: {'Stripe-Version': ApiVersion}, @@ -669,7 +669,7 @@ describe('RequestSender', () => { }); }); - it('throws a v2 StripeError based on the underlying error "code" if apiMode is v2', (done) => { + it('throws a v2 StripeError based on the underlying error "code" for v2 APIs', (done) => { const error = { type: 'temporary_session_expired', message: 'you messed up', @@ -688,7 +688,7 @@ describe('RequestSender', () => { }); }); - it('throws a v1 StripeError if apiMode is NOT v2', (done) => { + it('throws a v1 StripeError for v1 APIs', (done) => { const error = { type: 'temporary_session_expired', message: 'you messed up', From ef9378d70b351531e1739af3ce9657a7ce3a68fd Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Thu, 26 Sep 2024 13:02:47 -0700 Subject: [PATCH 16/20] Rename generate & generateV2 in Errors.ts to generateV1Error & generateV2Error for readability --- src/Error.ts | 12 ++++++++---- src/RequestSender.ts | 7 ++++--- test/Error.spec.ts | 10 +++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Error.ts b/src/Error.ts index b52aeeac5e..bfe2aee00a 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -3,7 +3,9 @@ import {RawErrorType, StripeRawError} from './Types.js'; -export const generate = (rawStripeError: StripeRawError): StripeError => { +export const generateV1Error = ( + rawStripeError: StripeRawError +): StripeError => { switch (rawStripeError.type) { case 'card_error': return new StripeCardError(rawStripeError); @@ -25,7 +27,9 @@ export const generate = (rawStripeError: StripeRawError): StripeError => { }; // eslint-disable-next-line complexity -export const generateV2 = (rawStripeError: StripeRawError): StripeError => { +export const generateV2Error = ( + rawStripeError: StripeRawError +): StripeError => { switch (rawStripeError.type) { // switchCases: The beginning of the section generated from our OpenAPI spec case 'temporary_session_expired': @@ -40,7 +44,7 @@ export const generateV2 = (rawStripeError: StripeRawError): StripeError => { return new StripeInvalidRequestError(rawStripeError); } - return generate(rawStripeError); + return generateV1Error(rawStripeError); }; /** @@ -98,7 +102,7 @@ export class StripeError extends Error { /** * Helper factory which takes raw stripe errors and outputs wrapping instances */ - static generate = generate; + static generate = generateV1Error; } // Specific Stripe Error types: diff --git a/src/RequestSender.ts b/src/RequestSender.ts index 06b13d3717..59bbaf5d2b 100644 --- a/src/RequestSender.ts +++ b/src/RequestSender.ts @@ -1,4 +1,3 @@ -import * as Errors from './Error.js'; import { StripeAPIError, StripeAuthenticationError, @@ -6,6 +5,8 @@ import { StripeError, StripePermissionError, StripeRateLimitError, + generateV1Error, + generateV2Error, } from './Error.js'; import { StripeObject, @@ -179,9 +180,9 @@ export class RequestSender { } else if (statusCode === 429) { err = new StripeRateLimitError(jsonResponse.error); } else if (apiMode === 'v2') { - err = Errors.generateV2(jsonResponse.error); + err = generateV2Error(jsonResponse.error); } else { - err = Errors.generate(jsonResponse.error); + err = generateV1Error(jsonResponse.error); } throw err; diff --git a/test/Error.spec.ts b/test/Error.spec.ts index 4a3774ee59..7efa021179 100644 --- a/test/Error.spec.ts +++ b/test/Error.spec.ts @@ -25,23 +25,23 @@ describe('Error', () => { it('Generates specific instance of v2 errors depending on error-type', () => { // Falls back to V1 parsing logic if code is absent - expect(Error.generateV2({type: 'card_error'})).to.be.instanceOf( + expect(Error.generateV2Error({type: 'card_error'})).to.be.instanceOf( Error.StripeCardError ); // Falls back to V1 parsing logic if code is unrecognized expect( - Error.generateV2({type: 'card_error', code: 'no_such_error'}) + Error.generateV2Error({type: 'card_error', code: 'no_such_error'}) ).to.be.instanceOf(Error.StripeCardError); expect( - Error.generateV2({ + Error.generateV2Error({ code: 'invalid_fields', }) ).to.be.instanceOf(Error.StripeInvalidRequestError); expect( - Error.generateV2({type: 'temporary_session_expired'}) + Error.generateV2Error({type: 'temporary_session_expired'}) ).to.be.instanceOf(Error.TemporarySessionExpiredError); - expect(Error.generateV2({code: 'invalid_fields'})).to.be.instanceOf( + expect(Error.generateV2Error({code: 'invalid_fields'})).to.be.instanceOf( Error.StripeInvalidRequestError ); }); From 5bee3ce670e55e0390fd48ae333136acad14d849 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Thu, 26 Sep 2024 16:06:43 -0700 Subject: [PATCH 17/20] updated README.me with clearer instructions --- examples/snippets/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/snippets/README.md b/examples/snippets/README.md index ce80f0454d..05216ba79f 100644 --- a/examples/snippets/README.md +++ b/examples/snippets/README.md @@ -1,12 +1,14 @@ ## Setup -1. From the project root, run `yarn build` to build the modules. -2. Run `yarn` to install node dependencies. This will reference the local Stripe SDK modules created in step 1. +1. From the stripe-node root folder, run `yarn build` to build the modules. +2. Then, from this snippets folder, run `yarn` to install node dependencies for the example snippets. This will reference the local Stripe SDK modules created in step 1. If on step 2 you see an error `Error: unsure how to copy this: /Users/jar/stripe/sdks/node/.git/fsmonitor--daemon.ipc`: run `rm /path/to/node/sdk/.git/fsmonitor--daemon.ipc && yarn` This file is used by a file monitor built into git. Removing it temporarily does not seem to affect its operation, and this one liner will let `yarn` succeed. +Note that if you modify the stripe-node code, you must delete your snippets `node_modules` folder and rerun these steps. + ## Running an example If your example is in typescript, run: From 934e0fa98ccdfa7b5fb715f4bc4e9f8e3b76bb4e Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Fri, 27 Sep 2024 01:17:09 -0700 Subject: [PATCH 18/20] Update generated code --- test/resources/generated_examples_test.spec.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/resources/generated_examples_test.spec.js b/test/resources/generated_examples_test.spec.js index a3da0e5a43..06c429bc1b 100644 --- a/test/resources/generated_examples_test.spec.js +++ b/test/resources/generated_examples_test.spec.js @@ -3001,17 +3001,6 @@ describe('Generated tests', function() { expect(reader).not.to.be.null; }); - it('test_terminal_readers_process_setup_intent_post', async function() { - const reader = await stripe.terminal.readers.processSetupIntent( - 'tmr_xxxxxxxxxxxxx', - { - setup_intent: 'seti_xxxxxxxxxxxxx', - customer_consent_collected: true, - } - ); - expect(reader).not.to.be.null; - }); - it('test_test_helpers_customers_fund_cash_balance_post', async function() { const customerCashBalanceTransaction = await stripe.testHelpers.customers.fundCashBalance( 'cus_123', From 215811476216e63d408689c30fe7de7b71701a6a Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 27 Sep 2024 11:17:27 -0700 Subject: [PATCH 19/20] fix ci --- testProjects/mjs/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testProjects/mjs/index.js b/testProjects/mjs/index.js index d5bc505f23..4271be4424 100644 --- a/testProjects/mjs/index.js +++ b/testProjects/mjs/index.js @@ -19,7 +19,8 @@ assert(Stripe.createNodeCryptoProvider); assert(Stripe.createSubtleCryptoProvider); assert(Stripe.errors); -assert(Stripe.errors.generate) +assert(Stripe.errors.generateV1Error); +assert(Stripe.errors.generateV2Error); assert(Stripe.errors.StripeError); assert(Stripe.errors.StripeCardError); assert(Stripe.errors.StripeInvalidRequestError); From 9d340c79cc4c1651729518f7c2b005221f9c63e1 Mon Sep 17 00:00:00 2001 From: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:19:02 -0700 Subject: [PATCH 20/20] =?UTF-8?q?Add=20related=5Fobject=20field=20to=20eve?= =?UTF-8?q?nt=20types,=20remove=20fetchRelatedObject()=20=E2=80=A6=20(#219?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Types.d.ts | 18 ------- src/resources/V2/Core/Events.ts | 52 +++----------------- src/stripe.core.ts | 24 ++++----- test/stripe.spec.ts | 11 ++++- types/ThinEvent.d.ts | 38 +++++++++++++++ types/V2/EventTypes.d.ts | 2 +- types/index.d.ts | 86 +++++++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 78 deletions(-) create mode 100644 types/ThinEvent.d.ts diff --git a/src/Types.d.ts b/src/Types.d.ts index 735a4f7b07..48f0ba9d85 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -1,12 +1,10 @@ /* eslint-disable camelcase */ import {EventEmitter} from 'events'; -import {CryptoProvider} from './crypto/CryptoProvider.js'; import { HttpClientInterface, HttpClientResponseInterface, } from './net/HttpClient.js'; import {PlatformFunctions} from './platform/PlatformFunctions.js'; -import {WebhookEvent} from './Webhooks.js'; export type AppInfo = {name?: string} & Record; export type ApiMode = 'v1' | 'v2'; @@ -138,22 +136,6 @@ export type StripeObject = { StripeResource: StripeResourceConstructor; errors: any; webhooks: any; - parseThinEvent: ( - payload: string | Uint8Array, - header: string | Uint8Array, - secret: string, - tolerance?: number, - cryptoProvider?: CryptoProvider, - receivedAt?: number - ) => WebhookEvent; - parseSnapshotEvent: ( - payload: string | Uint8Array, - header: string | Uint8Array, - secret: string, - tolerance?: number, - cryptoProvider?: CryptoProvider, - receivedAt?: number - ) => WebhookEvent; _prepResources: () => void; _setAppInfo: (appInfo: AppInfo) => void; _prevRequestMetrics: Array<{ diff --git a/src/resources/V2/Core/Events.ts b/src/resources/V2/Core/Events.ts index 1bde76d2ee..e0bba400ce 100644 --- a/src/resources/V2/Core/Events.ts +++ b/src/resources/V2/Core/Events.ts @@ -1,50 +1,12 @@ -// This file is manually maintained +// File generated from our OpenAPI spec import {StripeResource} from '../../../StripeResource.js'; - const stripeMethod = StripeResource.method; -const addFetchObjectToPulledEvent = (pulledEvent: { - related_object: any; - context?: string; -}): any => { - if (!pulledEvent.related_object || !pulledEvent.related_object.url) { - return pulledEvent; - } - return { - ...pulledEvent, - fetchRelatedObject: (): Promise => { - return stripeMethod({ - method: 'GET', - fullPath: pulledEvent.related_object.url, - })({ - stripeAccount: pulledEvent.context, - }); - }, - }; -}; - export const Events = StripeResource.extend({ - retrieve(...args: any[]) { - return stripeMethod({ - method: 'GET', - fullPath: '/v2/core/events/{id}', - }) - .apply(this, args) - .then((pulledEvent) => { - return addFetchObjectToPulledEvent(pulledEvent); - }); - }, - list(...args: any[]) { - stripeMethod({ - method: 'GET', - fullPath: '/v2/core/events', - methodType: 'list', - }) - .apply(this, args) - .then((pulledEvents) => { - return pulledEvents.map((pulledEvent: {related_object: {url: any}}) => { - return addFetchObjectToPulledEvent(pulledEvent); - }); - }); - }, + retrieve: stripeMethod({method: 'GET', fullPath: '/v2/core/events/{id}'}), + list: stripeMethod({ + method: 'GET', + fullPath: '/v2/core/events', + methodType: 'list', + }), }); diff --git a/src/stripe.core.ts b/src/stripe.core.ts index 77e9f3cc17..2a22fbd5d8 100644 --- a/src/stripe.core.ts +++ b/src/stripe.core.ts @@ -501,12 +501,12 @@ export function createStripe( }, parseThinEvent( - payload, - header, - secret, - tolerance, - cryptoProvider, - receivedAt + payload: string | Uint8Array, + header: string | Uint8Array, + secret: string, + tolerance?: number, + cryptoProvider?: CryptoProvider, + receivedAt?: number ): WebhookEvent { // parses and validates the event payload all in one go return this.webhooks.constructEvent( @@ -520,12 +520,12 @@ export function createStripe( }, parseSnapshotEvent( - payload, - header, - secret, - tolerance, - cryptoProvider, - receivedAt + payload: string | Uint8Array, + header: string | Uint8Array, + secret: string, + tolerance?: number, + cryptoProvider?: CryptoProvider, + receivedAt?: number ): WebhookEvent { return this.webhooks.constructEvent( payload, diff --git a/test/stripe.spec.ts b/test/stripe.spec.ts index 9f2503621c..69c840e71e 100644 --- a/test/stripe.spec.ts +++ b/test/stripe.spec.ts @@ -727,14 +727,21 @@ describe('Stripe Module', function() { const secret = 'whsec_test_secret'; it('can parse event from JSON payload', () => { - const payload = JSON.stringify({event_type: 'account.created'}); + const jsonPayload = { + type: 'account.created', + data: 'hello', + related_object: {id: '123', url: 'hello_again'}, + }; + const payload = JSON.stringify(jsonPayload); const header = stripe.webhooks.generateTestHeaderString({ payload, secret, }); const event = stripe.parseThinEvent(payload, header, secret); - expect(event.event_type).to.equal('account.created'); + expect(event.type).to.equal(jsonPayload.type); + expect(event.data).to.equal(jsonPayload.data); + expect(event.related_object.id).to.equal(jsonPayload.related_object.id); }); it('throws an error for invalid signatures', () => { diff --git a/types/ThinEvent.d.ts b/types/ThinEvent.d.ts new file mode 100644 index 0000000000..d1feb07c34 --- /dev/null +++ b/types/ThinEvent.d.ts @@ -0,0 +1,38 @@ +// This is a manually maintained file + +declare module 'stripe' { + namespace Stripe { + namespace Event { + interface RelatedObject { + /** + * Object containing the reference to API resource relevant to the event. + */ + related_object: { + /** + * Unique identifier for the object relevant to the event. + */ + id: string; + + /** + * Type of the object relevant to the event. + */ + type: string; + + /** + * URL to retrieve the resource. + */ + url: string; + }; + } + } + /** + * The Event object as recieved from StripeClient.parseThinEvent. + */ + interface ThinEvent extends V2.EventBase { + /** + * Object containing the reference to API resource relevant to the event. + */ + related_object: Event.RelatedObject; + } + } +} diff --git a/types/V2/EventTypes.d.ts b/types/V2/EventTypes.d.ts index a75f67a0ff..6b436f6691 100644 --- a/types/V2/EventTypes.d.ts +++ b/types/V2/EventTypes.d.ts @@ -17,7 +17,7 @@ declare module 'stripe' { // Retrieves data specific to this event. data: V1BillingMeterErrorReportTriggeredEvent.Data; // Retrieves the object associated with the event. - fetchRelatedObject(): Promise; + related_object: Event.RelatedObject; } namespace V1BillingMeterErrorReportTriggeredEvent { diff --git a/types/index.d.ts b/types/index.d.ts index 1b35492e52..5809b1d390 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -8,6 +8,8 @@ /// /// /// +/// +/// // Imports: The beginning of the section generated from our OpenAPI spec /// /// @@ -491,6 +493,90 @@ declare module 'stripe' { params?: {[key: string]: unknown}, options?: Stripe.RawRequestOptions ): Promise>; + + /** + * Parses webhook event payload into a ThinEvent and verifies webhook signature. + * To get more information on the event, pass the id from the returned object to + * `stripe.v2.core.events.retrieve()` + * + * @throws Stripe.errors.StripeSignatureVerificationError + */ + parseThinEvent: ( + /** + * Raw text body payload received from Stripe. + */ + payload: string | Buffer, + /** + * Value of the `stripe-signature` header from Stripe. + * Typically a string. + * + * Note that this is typed to accept an array of strings + * so that it works seamlessly with express's types, + * but will throw if an array is passed in practice + * since express should never return this header as an array, + * only a string. + */ + header: string | Buffer | Array, + /** + * Your Webhook Signing Secret for this endpoint (e.g., 'whsec_...'). + * You can get this [in your dashboard](https://dashboard.stripe.com/webhooks). + */ + secret: string, + /** + * Seconds of tolerance on timestamps. + */ + tolerance?: number, + /** + * Optional CryptoProvider to use for computing HMAC signatures. + */ + cryptoProvider?: Stripe.CryptoProvider, + + /** + * Optional: timestamp to use when checking signature validity. Defaults to Date.now(). + */ + receivedAt?: number + ) => Stripe.ThinEvent; + + /** + * Parses webhook event payload into a SnapshotEvent and verifies webhook signature. + * + * @throws Stripe.errors.StripeSignatureVerificationError + */ + parseSnapshotEvent: ( + /** + * Raw text body payload received from Stripe. + */ + payload: string | Buffer, + /** + * Value of the `stripe-signature` header from Stripe. + * Typically a string. + * + * Note that this is typed to accept an array of strings + * so that it works seamlessly with express's types, + * but will throw if an array is passed in practice + * since express should never return this header as an array, + * only a string. + */ + header: string | Buffer | Array, + /** + * Your Webhook Signing Secret for this endpoint (e.g., 'whsec_...'). + * You can get this [in your dashboard](https://dashboard.stripe.com/webhooks). + */ + secret: string, + /** + * Seconds of tolerance on timestamps. + */ + tolerance?: number, + /** + * Optional CryptoProvider to use for computing HMAC signatures. + */ + cryptoProvider?: Stripe.CryptoProvider, + + /** + * Optional: timestamp to use when checking signature validity. Defaults to Date.now(). + */ + receivedAt?: number + ) => Stripe.Event; } export default Stripe;