diff --git a/.changeset/loud-camels-sell.md b/.changeset/loud-camels-sell.md new file mode 100644 index 000000000..f2521f451 --- /dev/null +++ b/.changeset/loud-camels-sell.md @@ -0,0 +1,7 @@ +--- +"@shopify/shopify-api": minor +--- + +Added support for validating Flow extension requests, using `shopify.authenticate.flow`. + +Please see [the `flow` object documentation](./docs/reference/flow/README.md) for more information. diff --git a/.changeset/odd-poems-smash.md b/.changeset/odd-poems-smash.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/odd-poems-smash.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/markdown_link_check.yml b/.github/workflows/markdown_link_check.yml index 0613017cc..829019962 100644 --- a/.github/workflows/markdown_link_check.yml +++ b/.github/workflows/markdown_link_check.yml @@ -10,3 +10,4 @@ jobs: - uses: gaurav-nelson/github-action-markdown-link-check@v1 with: config-file: '.github/workflows/markdown_link_checker_config.json' + folder-path: 'packages' diff --git a/packages/shopify-api/docs/reference/flow/README.md b/packages/shopify-api/docs/reference/flow/README.md new file mode 100644 index 000000000..28ffd29c7 --- /dev/null +++ b/packages/shopify-api/docs/reference/flow/README.md @@ -0,0 +1,9 @@ +# shopify.flow + +This object contains functions used to authenticate Flow extension requests coming from Shopify. + +| Property | Description | +| ------------------------- | ------------------------------------------------------------------- | +| [validate](./validate.md) | Verify whether a request is a valid Shopify Flow extension request. | + +[Back to shopifyApi](../shopifyApi.md) diff --git a/packages/shopify-api/docs/reference/flow/validate.md b/packages/shopify-api/docs/reference/flow/validate.md new file mode 100644 index 000000000..44ca6594f --- /dev/null +++ b/packages/shopify-api/docs/reference/flow/validate.md @@ -0,0 +1,66 @@ +# shopify.flow.validate + +Takes in a raw request and the raw body for that request, and validates that it's a legitimate Shopify Flow extension request. + +Refer to [the Flow documentation](https://shopify.dev/docs/apps/flow/actions/endpoints#custom-validation) for more information on how this validation works. + +## Example + +```ts +app.post('/flow', express.text({type: '*/*'}), async (req, res) => { + const result = await shopify.flow.validate({ + rawBody: req.body, // is a string + rawRequest: req, + rawResponse: res, + }); + + if (!result.valid) { + console.log(`Received invalid Flow extension request: ${result.reason}`); + res.send(400); + } + + res.send(200); +}); +``` + +## Parameters + +Receives an object containing: + +### rawBody + +`string` | :exclamation: required + +The raw body of the request received by the app. + +### rawRequest + +`AdapterRequest` | :exclamation: required + +The HTTP Request object used by your runtime. + +### rawResponse + +`AdapterResponse` | :exclamation: required for Node.js + +The HTTP Response object used by your runtime. Required for Node.js. + +## Return + +Returns an object containing: + +### valid + +`boolean` + +Whether the request is a valid Flow extension request from Shopify. + +### If valid is `false`: + +#### reason + +`FlowValidationErrorReason` + +The reason why the check was considered invalid. + +[Back to shopify.flow](./README.md) diff --git a/packages/shopify-api/docs/reference/shopifyApi.md b/packages/shopify-api/docs/reference/shopifyApi.md index 38fe6b721..40f43a394 100644 --- a/packages/shopify-api/docs/reference/shopifyApi.md +++ b/packages/shopify-api/docs/reference/shopifyApi.md @@ -172,6 +172,7 @@ This function returns an object containing the following properties: | [session](./session/README.md) | Object containing functions to manage Shopify sessions. | | [webhooks](./webhooks/README.md) | Object containing functions to configure and handle Shopify webhooks. | | [billing](./billing/README.md) | Object containing functions to enable apps to bill merchants. | +| [flow](./flow/README.md) | Object containing functions to authenticate Flow extension requests. | | [utils](./utils/README.md) | Object containing general functions to help build apps. | | [rest](../guides/rest-resources.md) | Object containing OO representations of the Admin REST API. See the [API reference documentation](https://shopify.dev/docs/api/admin-rest) for details. | diff --git a/packages/shopify-api/future/flags.ts b/packages/shopify-api/future/flags.ts index bdbaf20e5..13ecc3d87 100644 --- a/packages/shopify-api/future/flags.ts +++ b/packages/shopify-api/future/flags.ts @@ -1,8 +1,20 @@ +/** + * Future flags are used to enable features that are not yet available by default. + */ export interface FutureFlags { + /** + * Enable the token exchange OAuth flow. + */ unstable_tokenExchange?: boolean; + /** + * Enable line item billing, to make billing configuration more similar to the GraphQL API. + */ unstable_lineItemBilling?: boolean; } +/** + * Configuration option for future flags. + */ export type FutureFlagOptions = FutureFlags | undefined; export type FeatureEnabled< diff --git a/packages/shopify-api/lib/auth/oauth/types.ts b/packages/shopify-api/lib/auth/oauth/types.ts index f47027898..b29e0833c 100644 --- a/packages/shopify-api/lib/auth/oauth/types.ts +++ b/packages/shopify-api/lib/auth/oauth/types.ts @@ -23,16 +23,49 @@ export interface AccessTokenResponse { } export interface OnlineAccessInfo { + /** + * How long the access token is valid for, in seconds. + */ expires_in: number; + /** + * The effective set of scopes for the session. + */ associated_user_scope: string; + /** + * The user associated with the access token. + */ associated_user: { + /** + * The user's ID. + */ id: number; + /** + * The user's first name. + */ first_name: string; + /** + * The user's last name. + */ last_name: string; + /** + * The user's email address. + */ email: string; + /** + * Whether the user has verified their email address. + */ email_verified: boolean; + /** + * Whether the user is the account owner. + */ account_owner: boolean; + /** + * The user's locale. + */ locale: string; + /** + * Whether the user is a collaborator. + */ collaborator: boolean; }; } diff --git a/packages/shopify-api/lib/auth/scopes/index.ts b/packages/shopify-api/lib/auth/scopes/index.ts index da91e45db..41c16179e 100644 --- a/packages/shopify-api/lib/auth/scopes/index.ts +++ b/packages/shopify-api/lib/auth/scopes/index.ts @@ -1,3 +1,6 @@ +/** + * A class that represents a set of access token scopes. + */ class AuthScopes { public static SCOPE_DELIMITER = ','; @@ -31,6 +34,9 @@ class AuthScopes { this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } + /** + * Checks whether the current set of scopes includes the given one. + */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; @@ -45,6 +51,9 @@ class AuthScopes { ); } + /** + * Checks whether the current set of scopes equals the given one. + */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; @@ -60,10 +69,16 @@ class AuthScopes { ); } + /** + * Returns a comma-separated string with the current set of scopes. + */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } + /** + * Returns an array with the current set of scopes. + */ public toArray() { return [...this.compressedScopes]; } diff --git a/packages/shopify-api/lib/base-types.ts b/packages/shopify-api/lib/base-types.ts index 33ccd23b2..59a63a286 100644 --- a/packages/shopify-api/lib/base-types.ts +++ b/packages/shopify-api/lib/base-types.ts @@ -5,32 +5,107 @@ import {AuthScopes} from './auth/scopes'; import {BillingConfig} from './billing/types'; import {ApiVersion, LogSeverity} from './types'; +/** + * A function used by the library to log events related to Shopify. + */ export type LogFunction = (severity: LogSeverity, msg: string) => void; export interface ConfigParams< Resources extends ShopifyRestResources = ShopifyRestResources, Future extends FutureFlagOptions = FutureFlagOptions, > { + /** + * The API key for your app. + * + * Also known as Client ID in your Partner Dashboard. + */ apiKey?: string; + /** + * The API secret key for your app. + * + * Also known as Client Secret in your Partner Dashboard. + */ apiSecretKey: string; + /** + * The scopes your app needs to access the API. + */ scopes?: string[] | AuthScopes; + /** + * The host name of your app. + */ hostName: string; + /** + * The scheme to use for the app host. + */ hostScheme?: 'http' | 'https'; + /** + * The API version to use. + */ apiVersion: ApiVersion; + /** + * Whether the app is embedded in the Shopify admin. + */ isEmbeddedApp: boolean; + /** + * Whether the app is a Shopify admin custom store app. + * + * @link https://shopify.dev/docs/apps/distribution + */ isCustomStoreApp?: boolean; + /** + * An app-wide API access token. + * + * Only applies to custom apps. + */ adminApiAccessToken?: string; + /** + * The user agent prefix to use for API requests. + */ userAgentPrefix?: string; + /** + * An app-wide API access token for the storefront API. + * + * Only applies to custom apps. + */ privateAppStorefrontAccessToken?: string; + /** + * Override values for Shopify shop domains. + */ customShopDomains?: (RegExp | string)[]; + /** + * Billing configurations for the app. + */ billing?: BillingConfig; + /** + * REST resources to access the Admin API. + * + * You can import these from `@shopify/shopify-api/rest/admin/*`. + */ restResources?: Resources; + /** + * Customization options for Shopify logs. + */ logger?: { + /** + * A custom log function. + */ log?: LogFunction; + /** + * The minimum severity level to log. + */ level?: LogSeverity; + /** + * Whether to log HTTP requests. + */ httpRequests?: boolean; + /** + * Whether to log timestamps. + */ timestamps?: boolean; }; + /** + * Future flags to include for this app. + */ future?: Future; } diff --git a/packages/shopify-api/lib/billing/types.ts b/packages/shopify-api/lib/billing/types.ts index e5bb0d73a..ea4cc72a3 100644 --- a/packages/shopify-api/lib/billing/types.ts +++ b/packages/shopify-api/lib/billing/types.ts @@ -7,42 +7,107 @@ import {Session} from '../session/session'; import {FeatureEnabled, FutureFlagOptions} from '../../future/flags'; export interface BillingConfigPlan { + /** + * Amount to charge for this plan. + */ amount: number; + /** + * Currency code for this plan. + */ currencyCode: string; } export interface BillingConfigOneTimePlan extends BillingConfigPlan { + /** + * Interval for this plan. + * + * Must be set to `OneTime`. + */ interval: BillingInterval.OneTime; } export interface BillingConfigSubscriptionPlan extends BillingConfigPlan { + /** + * Recurring interval for this plan. + * + * Must be either `Every30Days` or `Annual`. + */ interval: Exclude; + /** + * How many trial days to give before charging for this plan. + */ trialDays?: number; + /** + * The behavior to use when replacing an existing subscription with a new one. + */ replacementBehavior?: BillingReplacementBehavior; + /** + * The discount to apply to this plan. + */ discount?: BillingConfigSubscriptionPlanDiscount; } export interface BillingConfigSubscriptionPlanDiscountAmount { + /** + * The amount to discount. + * + * Cannot be set if `percentage` is set. + */ amount: number; + /** + * The percentage to discount. + * + * Cannot be set if `amount` is set. + */ percentage?: never; } export interface BillingConfigSubscriptionPlanDiscountPercentage { + /** + * The amount to discount. + * + * Cannot be set if `percentage` is set. + */ amount?: never; + /** + * The percentage to discount. + * + * Cannot be set if `amount` is set. + */ percentage: number; } export interface BillingConfigSubscriptionPlanDiscount { + /** + * The number of intervals to apply the discount for. + */ durationLimitInIntervals?: number; + /** + * The discount to apply. + */ value: | BillingConfigSubscriptionPlanDiscountAmount | BillingConfigSubscriptionPlanDiscountPercentage; } export interface BillingConfigUsagePlan extends BillingConfigPlan { + /** + * Interval for this plan. + * + * Must be set to `Usage`. + */ interval: BillingInterval.Usage; + /** + * Usage terms for this plan. + */ usageTerms: string; + /** + * How many trial days to give before charging for this plan. + */ trialDays?: number; + /** + * The behavior to use when replacing an existing subscription with a new one. + */ replacementBehavior?: BillingReplacementBehavior; } @@ -57,9 +122,20 @@ export type BillingConfigItem< ? BillingConfigOneTimePlan | BillingConfigSubscriptionLineItemPlan : BillingConfigLegacyItem; -export type BillingConfig< +// Type this as an interface to improve TSDoc support for it. +/* eslint-disable @typescript-eslint/consistent-indexed-object-style */ +/** + * Billing configuration options, indexed by an app-specific plan name. + */ +export interface BillingConfig< Future extends FutureFlagOptions = FutureFlagOptions, -> = Record>; +> { + /** + * An individual billing plan. + */ + [plan: string]: BillingConfigItem; +} +/* eslint-enable @typescript-eslint/consistent-indexed-object-style */ export type RequestConfigOverrides = | Partial @@ -67,23 +143,54 @@ export type RequestConfigOverrides = | Partial; export interface BillingConfigLineItem { + /** + * The amount to charge for this line item. + */ amount: number; + /** + * The currency code for this line item. + */ currencyCode: string; } export interface BillingConfigRecurringLineItem extends BillingConfigLineItem { + /** + * The recurring interval for this line item. + * + * Must be either `Every30Days` or `Annual`. + */ interval: BillingInterval.Every30Days | BillingInterval.Annual; + /** + * An optional discount to apply for this line item. + */ discount?: BillingConfigSubscriptionPlanDiscount; } export interface BillingConfigUsageLineItem extends BillingConfigLineItem { + /** + * The usage interval for this line item. + * + * Must be set to `Usage`. + */ interval: BillingInterval.Usage; + /** + * Usage terms for this line item. + */ terms: string; } export interface BillingConfigSubscriptionLineItemPlan { + /** + * The replacement behavior to use for this plan. + */ replacementBehavior?: BillingReplacementBehavior; + /** + * How many trial days to give before charging for this plan. + */ trialDays?: number; + /** + * The line items for this plan. + */ lineItems: (BillingConfigRecurringLineItem | BillingConfigUsageLineItem)[]; } @@ -91,15 +198,36 @@ export type RequestConfigLineItemOverrides = Partial; export interface BillingCheckParams { + /** + * The session to use for this check. + */ session: Session; + /** + * The plans to accept for this check. + */ plans: string[] | string; + /** + * Whether to consider test purchases. + */ isTest?: boolean; + /** + * Whether to return the full response object. + */ returnObject?: boolean; } export interface BillingCheckResponseObject { + /** + * Whether the user has an active payment method. + */ hasActivePayment: boolean; + /** + * The one-time purchases the shop has. + */ oneTimePurchases: OneTimePurchase[]; + /** + * The active subscriptions the shop has. + */ appSubscriptions: AppSubscription[]; } @@ -107,16 +235,40 @@ export type BillingCheckResponse = Params['returnObject'] extends true ? BillingCheckResponseObject : boolean; export type BillingRequestParams = { + /** + * The session to use for this request. + */ session: Session; + /** + * The plan to request. + */ plan: string; + /** + * Whether this is a test purchase. + */ isTest?: boolean; + /** + * Override the return URL after the purchase is complete. + */ returnUrl?: string; + /** + * Whether to return the full response object. + */ returnObject?: boolean; } & RequestConfigOverrides; export interface BillingRequestResponseObject { + /** + * The confirmation URL for this request. + */ confirmationUrl: string; + /** + * The one-time purchase created by this request. + */ oneTimePurchase?: OneTimePurchase; + /** + * The app subscription created by this request. + */ appSubscription?: AppSubscription; } @@ -124,19 +276,43 @@ export type BillingRequestResponse = Params['returnObject'] extends true ? BillingRequestResponseObject : string; export interface BillingCancelParams { + /** + * The session to use for this request. + */ session: Session; + /** + * The subscription ID to cancel. + */ subscriptionId: string; + /** + * Whether to prorate the cancellation. + */ prorate?: boolean; + /** + * Whether to consider test purchases. + */ isTest?: boolean; } export interface BillingSubscriptionParams { + /** + * The session to use for this request. + */ session: Session; } export interface AppSubscription { + /** + * The ID of the app subscription. + */ id: string; + /** + * The name of the purchased plan. + */ name: string; + /** + * Whether this is a test subscription. + */ test: boolean; } @@ -145,9 +321,21 @@ export interface ActiveSubscriptions { } export interface OneTimePurchase { + /** + * The ID of the one-time purchase. + */ id: string; + /** + * The name of the purchased plan. + */ name: string; + /** + * Whether this is a test purchase. + */ test: boolean; + /** + * The status of the one-time purchase. + */ status: string; } diff --git a/packages/shopify-api/lib/clients/types.ts b/packages/shopify-api/lib/clients/types.ts index cd91f1453..f2f6f3bc7 100644 --- a/packages/shopify-api/lib/clients/types.ts +++ b/packages/shopify-api/lib/clients/types.ts @@ -23,6 +23,9 @@ export interface ClientArgs { retries?: number; } +/** + * Headers to be sent with the request. + */ export type HeaderParams = Record; /* eslint-disable @shopify/typescript/prefer-pascal-case-enums */ @@ -34,11 +37,29 @@ export enum DataType { /* eslint-enable @shopify/typescript/prefer-pascal-case-enums */ export interface GetRequestParams { + /** + * The path to the resource, relative to the API version root. + */ path: string; + /** + * The type of data expected in the response. + */ type?: DataType; + /** + * The request body. + */ data?: Record | string; + /** + * Query parameters to be sent with the request. + */ query?: SearchParams; + /** + * Additional headers to be sent with the request. + */ extraHeaders?: HeaderParams; + /** + * The maximum number of times the request can be made if it fails with a throttling or server error. + */ tries?: number; } @@ -55,7 +76,13 @@ export type RequestParams = (GetRequestParams | PostRequestParams) & { }; export interface RequestReturn { + /** + * The response body. + */ body: T; + /** + * The response headers. + */ headers: Headers; } @@ -70,8 +97,17 @@ export interface GraphqlQueryOptions< Operation extends keyof Operations, Operations extends AllOperations, > { + /** + * The variables to include in the operation. + */ variables?: ApiClientRequestOptions['variables']; + /** + * Additional headers to be sent with the request. + */ headers?: Record; + /** + * The maximum number of times to retry the request if it fails with a throttling or server error. + */ retries?: number; } diff --git a/packages/shopify-api/lib/flow/__tests__/flow.test.ts b/packages/shopify-api/lib/flow/__tests__/flow.test.ts new file mode 100644 index 000000000..a5cc53184 --- /dev/null +++ b/packages/shopify-api/lib/flow/__tests__/flow.test.ts @@ -0,0 +1,119 @@ +import {shopifyApi} from '../..'; +import {ShopifyHeader} from '../../types'; +import { + createSHA256HMAC, + HashFormat, + type NormalizedRequest, +} from '../../../runtime'; +import {testConfig} from '../../__tests__/test-config'; +import {FlowValidationErrorReason} from '../types'; + +describe('flow', () => { + describe('validate', () => { + describe('failure cases', () => { + it('fails if the HMAC header is missing', async () => { + // GIVEN + const shopify = shopifyApi(testConfig()); + + const payload = {field: 'value'}; + const req: NormalizedRequest = { + method: 'GET', + url: 'https://my-app.my-domain.io', + headers: {}, + }; + + // WHEN + const result = await shopify.flow.validate({ + rawBody: JSON.stringify(payload), + rawRequest: req, + }); + + // THEN + expect(result).toMatchObject({ + valid: false, + reason: FlowValidationErrorReason.MissingHmac, + }); + }); + + it('fails if the HMAC header is invalid', async () => { + // GIVEN + const shopify = shopifyApi(testConfig()); + + const payload = {field: 'value'}; + const req: NormalizedRequest = { + method: 'GET', + url: 'https://my-app.my-domain.io', + headers: {[ShopifyHeader.Hmac]: 'invalid'}, + }; + + // WHEN + const result = await shopify.flow.validate({ + rawBody: JSON.stringify(payload), + rawRequest: req, + }); + + // THEN + expect(result).toMatchObject({ + valid: false, + reason: FlowValidationErrorReason.InvalidHmac, + }); + }); + + it('fails if the body is empty', async () => { + // GIVEN + const shopify = shopifyApi(testConfig()); + + const req: NormalizedRequest = { + method: 'GET', + url: 'https://my-app.my-domain.io', + headers: { + [ShopifyHeader.Hmac]: await createSHA256HMAC( + shopify.config.apiSecretKey, + '', + HashFormat.Base64, + ), + }, + }; + + // WHEN + const result = await shopify.flow.validate({ + rawBody: '', + rawRequest: req, + }); + + // THEN + expect(result).toMatchObject({ + valid: false, + reason: FlowValidationErrorReason.MissingBody, + }); + }); + }); + + it('succeeds if the body and HMAC header are correct', async () => { + // GIVEN + const shopify = shopifyApi(testConfig()); + + const payload = {field: 'value'}; + const req: NormalizedRequest = { + method: 'GET', + url: 'https://my-app.my-domain.io', + headers: { + [ShopifyHeader.Hmac]: await createSHA256HMAC( + shopify.config.apiSecretKey, + JSON.stringify(payload), + HashFormat.Base64, + ), + }, + }; + + // WHEN + const result = await shopify.flow.validate({ + rawBody: JSON.stringify(payload), + rawRequest: req, + }); + + // THEN + expect(result).toMatchObject({valid: true}); + }); + }); +}); diff --git a/packages/shopify-api/lib/flow/index.ts b/packages/shopify-api/lib/flow/index.ts new file mode 100644 index 000000000..c10293c9a --- /dev/null +++ b/packages/shopify-api/lib/flow/index.ts @@ -0,0 +1,11 @@ +import {ConfigInterface} from '../base-types'; + +import {validateFactory} from './validate'; + +export function shopifyFlow(config: ConfigInterface) { + return { + validate: validateFactory(config), + }; +} + +export type ShopifyFlow = ReturnType; diff --git a/packages/shopify-api/lib/flow/types.ts b/packages/shopify-api/lib/flow/types.ts new file mode 100644 index 000000000..f7ab43ebb --- /dev/null +++ b/packages/shopify-api/lib/flow/types.ts @@ -0,0 +1,32 @@ +import {AdapterArgs} from '../../runtime/types'; + +export interface FlowValidateParams extends AdapterArgs { + /** + * The raw body of the request. + */ + rawBody: string; +} + +export enum FlowValidationErrorReason { + MissingBody = 'missing_body', + MissingHmac = 'missing_hmac', + InvalidHmac = 'invalid_hmac', +} + +export interface FlowValidationInvalid { + /** + * Whether the request is a valid Flow request from Shopify. + */ + valid: false; + /** + * The reason why the request is not valid. + */ + reason: FlowValidationErrorReason; +} + +export interface FlowValidationValid { + /** + * Whether the request is a valid Flow request from Shopify. + */ + valid: true; +} diff --git a/packages/shopify-api/lib/flow/validate.ts b/packages/shopify-api/lib/flow/validate.ts new file mode 100644 index 000000000..421b3b63c --- /dev/null +++ b/packages/shopify-api/lib/flow/validate.ts @@ -0,0 +1,60 @@ +import {abstractConvertRequest, getHeader} from '../../runtime/http'; +import {HashFormat} from '../../runtime/crypto/types'; +import {ConfigInterface} from '../base-types'; +import {logger} from '../logger'; +import {ShopifyHeader} from '../types'; +import {validateHmacString} from '../utils/hmac-validator'; + +import { + FlowValidateParams, + FlowValidationInvalid, + FlowValidationValid, + FlowValidationErrorReason, +} from './types'; + +export function validateFactory(config: ConfigInterface) { + return async function validate({ + rawBody, + ...adapterArgs + }: FlowValidateParams): Promise { + const request = await abstractConvertRequest(adapterArgs); + + if (!rawBody.length) { + return fail(FlowValidationErrorReason.MissingBody, config); + } + + const hmac = getHeader(request.headers, ShopifyHeader.Hmac); + + if (!hmac) { + return fail(FlowValidationErrorReason.MissingHmac, config); + } + + if (await validateHmacString(config, rawBody, hmac, HashFormat.Base64)) { + return succeed(config); + } + + return fail(FlowValidationErrorReason.InvalidHmac, config); + }; +} + +async function fail( + reason: FlowValidationErrorReason, + config: ConfigInterface, +): Promise { + const log = logger(config); + await log.debug('Flow request is not valid', {reason}); + + return { + valid: false, + reason, + }; +} + +async function succeed(config: ConfigInterface): Promise { + const log = logger(config); + await log.debug('Flow request is valid'); + + return { + valid: true, + }; +} diff --git a/packages/shopify-api/lib/index.ts b/packages/shopify-api/lib/index.ts index 957ef2e1f..0107aaed2 100644 --- a/packages/shopify-api/lib/index.ts +++ b/packages/shopify-api/lib/index.ts @@ -14,6 +14,7 @@ import {shopifyBilling, ShopifyBilling} from './billing'; import {logger, ShopifyLogger} from './logger'; import {SHOPIFY_API_LIBRARY_VERSION} from './version'; import {restClientClass} from './clients/admin/rest/client'; +import {ShopifyFlow, shopifyFlow} from './flow'; export * from './error'; export * from './session/classes'; @@ -26,6 +27,7 @@ export * from './billing/types'; export * from './clients/types'; export * from './session/types'; export * from './webhooks/types'; +export * from './flow/types'; export interface Shopify< Params extends ConfigParams = ConfigParams, @@ -41,6 +43,7 @@ export interface Shopify< billing: ShopifyBilling; logger: ShopifyLogger; rest: Resources; + flow: ShopifyFlow; } export function shopifyApi< @@ -67,6 +70,7 @@ export function shopifyApi< utils: shopifyUtils(validatedConfig), webhooks: shopifyWebhooks(validatedConfig), billing: shopifyBilling(validatedConfig), + flow: shopifyFlow(validatedConfig), logger: logger(validatedConfig), rest: {} as Resources, }; diff --git a/packages/shopify-api/lib/session/session.ts b/packages/shopify-api/lib/session/session.ts index 875a97701..568e8fe95 100644 --- a/packages/shopify-api/lib/session/session.ts +++ b/packages/shopify-api/lib/session/session.ts @@ -75,19 +75,46 @@ export class Session { return obj; } + /** + * The unique identifier for the session. + */ readonly id: string; + /** + * The Shopify shop domain, such as `example.myshopify.com`. + */ public shop: string; + /** + * The state of the session. Used for the OAuth authentication code flow. + */ public state: string; + /** + * Whether the access token in the session is online or offline. + */ public isOnline: boolean; + /** + * The desired scopes for the access token, at the time the session was created. + */ public scope?: string; + /** + * The date the access token expires. + */ public expires?: Date; + /** + * The access token for the session. + */ public accessToken?: string; + /** + * Information on the user for the session. Only present for online sessions. + */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } + /** + * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. + */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && @@ -96,6 +123,9 @@ export class Session { ); } + /** + * Whether the access token has the given scopes. + */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); @@ -103,6 +133,9 @@ export class Session { return !scopesObject.equals(this.scope); } + /** + * Whether the access token is expired. + */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && @@ -110,6 +143,9 @@ export class Session { ); } + /** + * Converts an object with data into a Session. + */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, @@ -133,6 +169,9 @@ export class Session { return object; } + /** + * Checks whether the given session is equal to this session. + */ public equals(other: Session | undefined): boolean { if (!other) return false; @@ -153,6 +192,9 @@ export class Session { return JSON.stringify(copyA) === JSON.stringify(copyB); } + /** + * Converts the session into an array of key-value pairs. + */ public toPropertyArray(): [string, string | number | boolean][] { return ( Object.entries(this) diff --git a/packages/shopify-api/lib/session/types.ts b/packages/shopify-api/lib/session/types.ts index 18bff3a43..be3347dcd 100644 --- a/packages/shopify-api/lib/session/types.ts +++ b/packages/shopify-api/lib/session/types.ts @@ -2,25 +2,76 @@ import {AdapterArgs} from '../../runtime/http'; import {OnlineAccessInfo} from '../auth/oauth/types'; export interface SessionParams { + /** + * The unique identifier for the session. + */ readonly id: string; + /** + * The Shopify shop domain. + */ shop: string; + /** + * The state of the session. Used for the OAuth authentication code flow. + */ state: string; + /** + * Whether the access token in the session is online or offline. + */ isOnline: boolean; + /** + * The scopes for the access token. + */ scope?: string; + /** + * The date the access token expires. + */ expires?: Date; + /** + * The access token for the session. + */ accessToken?: string; + /** + * Information on the user for the session. Only present for online sessions. + */ onlineAccessInfo?: OnlineAccessInfo; } export interface JwtPayload { + /** + * The shop's admin domain. + */ iss: string; + /** + * The shop's domain. + */ dest: string; + /** + * The client ID of the receiving app. + */ aud: string; + /** + * The User that the session token is intended for. + */ sub: string; + /** + * When the session token expires. + */ exp: number; + /** + * When the session token activates. + */ nbf: number; + /** + * When the session token was issued. + */ iat: number; + /** + * A secure random UUID. + */ jti: string; + /** + * A unique session ID per user and app. + */ sid: string; } diff --git a/packages/shopify-api/lib/utils/hmac-validator.ts b/packages/shopify-api/lib/utils/hmac-validator.ts index 1d5d08c14..848bfcea1 100644 --- a/packages/shopify-api/lib/utils/hmac-validator.ts +++ b/packages/shopify-api/lib/utils/hmac-validator.ts @@ -70,6 +70,17 @@ export function validateHmac(config: ConfigInterface) { }; } +export async function validateHmacString( + config: ConfigInterface, + data: string, + hmac: string, + format: HashFormat, +) { + const localHmac = await createSHA256HMAC(config.apiSecretKey, data, format); + + return safeCompare(hmac, localHmac); +} + export function getCurrentTimeInSec() { return Math.trunc(Date.now() / 1000); } diff --git a/packages/shopify-api/lib/webhooks/validate.ts b/packages/shopify-api/lib/webhooks/validate.ts index ca19fec17..1ef465631 100644 --- a/packages/shopify-api/lib/webhooks/validate.ts +++ b/packages/shopify-api/lib/webhooks/validate.ts @@ -4,12 +4,11 @@ import { Headers, NormalizedRequest, } from '../../runtime/http'; -import {createSHA256HMAC} from '../../runtime/crypto'; -import {HashFormat} from '../../runtime/crypto/types'; import {ShopifyHeader} from '../types'; import {ConfigInterface} from '../base-types'; -import {safeCompare} from '../auth/oauth/safe-compare'; import {logger} from '../logger'; +import {validateHmacString} from '../utils/hmac-validator'; +import {HashFormat} from '../../runtime'; import { WebhookFields, @@ -46,7 +45,7 @@ export function validateFactory(config: ConfigInterface) { const {hmac, valid: _valid, ...loggingContext} = webhookCheck; await log.debug('Webhook request is well formed', loggingContext); - if (await checkWebhookHmac(config.apiSecretKey, rawBody, hmac)) { + if (await validateHmacString(config, rawBody, hmac, HashFormat.Base64)) { await log.debug('Webhook request is valid', loggingContext); return webhookCheck; } else { @@ -102,17 +101,3 @@ function checkWebhookRequest( }; } } - -async function checkWebhookHmac( - secret: string, - rawBody: string, - hmac: string, -): Promise { - const generatedHash = await createSHA256HMAC( - secret, - rawBody, - HashFormat.Base64, - ); - - return safeCompare(generatedHash, hmac); -} diff --git a/packages/shopify-api/package.json b/packages/shopify-api/package.json index cb050ad64..34569670f 100644 --- a/packages/shopify-api/package.json +++ b/packages/shopify-api/package.json @@ -89,7 +89,7 @@ "@types/supertest": "^2.0.10", "@types/uuid": "^9.0.0", "express": "^4.17.13", - "jest-environment-miniflare": "^2.12.1", + "jest-environment-miniflare": "^2.14.2", "miniflare": "^3.20231218.3", "rollup": "^2.79.1", "rollup-plugin-swc": "^0.2.1", diff --git a/packages/shopify-api/runtime/http/types.ts b/packages/shopify-api/runtime/http/types.ts index 6f8ba13cb..fc3401c59 100644 --- a/packages/shopify-api/runtime/http/types.ts +++ b/packages/shopify-api/runtime/http/types.ts @@ -18,7 +18,14 @@ export type AdapterRequest = any; export type AdapterResponse = any; export type AdapterHeaders = any; export interface AdapterArgs { + /** + * The raw request, from the app's framework. + */ rawRequest: AdapterRequest; + /** + * The raw response, from the app's framework. Only applies to frameworks that expose an API similar to Node's HTTP + * module. + */ rawResponse?: AdapterResponse; } diff --git a/yarn.lock b/yarn.lock index 5ee4ffbfc..f029242e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2383,158 +2383,158 @@ globby "^11.0.0" read-yaml-file "^1.1.0" -"@miniflare/cache@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/cache/-/cache-2.14.1.tgz#2c6a165caca63a39b1aa56c5152a583be8330398" - integrity sha512-f/o6UBV6UX+MlhjcEch73/wjQvvNo37dgYmP6Pn2ax1/mEHhJ7allNAqenmonT4djNeyB3eEYV3zUl54wCEwrg== +"@miniflare/cache@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/cache/-/cache-2.14.2.tgz#626d3eb899eeb850f3afd6bcdce452aa4234e333" + integrity sha512-XH218Y2jxSOfxG8EyuprBKhI/Fn6xLrb9A39niJBlzpiKXqr8skl/sy/sUL5tfvqEbEnqDagGne8zEcjM+1fBg== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" http-cache-semantics "^4.1.0" - undici "5.20.0" + undici "5.28.2" -"@miniflare/core@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/core/-/core-2.14.1.tgz#545199da6598c8295e24f629a1aa1d44bad1f6ba" - integrity sha512-d+SGAda/VoXq+SKz04oq8ATUwQw5755L87fgPR8pTdR2YbWkxdbmEm1z2olOpDiUjcR86aN6NtCjY6tUC7fqaw== +"@miniflare/core@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/core/-/core-2.14.2.tgz#6a08738cf2f72ea60f3b92c5ce3fb974dbaf6122" + integrity sha512-n/smm5ZTg7ilGM4fxO7Gxhbe573oc8Za06M3b2fO+lPWqF6NJcEKdCC+sJntVFbn3Cbbd2G1ChISmugPfmlCkQ== dependencies: "@iarna/toml" "^2.2.5" - "@miniflare/queues" "2.14.1" - "@miniflare/shared" "2.14.1" - "@miniflare/watcher" "2.14.1" + "@miniflare/queues" "2.14.2" + "@miniflare/shared" "2.14.2" + "@miniflare/watcher" "2.14.2" busboy "^1.6.0" dotenv "^10.0.0" kleur "^4.1.4" set-cookie-parser "^2.4.8" - undici "5.20.0" + undici "5.28.2" urlpattern-polyfill "^4.0.3" -"@miniflare/d1@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/d1/-/d1-2.14.1.tgz#016ec975981c831ef5e036d2243dc678cec6ab4e" - integrity sha512-MulDDBsDD8o5DwiqdMeJZy2vLoMji+NWnLcuibSag2mayA0LJcp0eHezseZNkW+knciWR1gMP8Xpa4Q1KwkbKA== +"@miniflare/d1@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/d1/-/d1-2.14.2.tgz#0bb21db0a94e1a5ecffacc289da78aa65f02530a" + integrity sha512-3NPJyBLbFfzz9VAAdIZrDRdRpyslVCJoZHQk0/0CX3z2mJIfcQzjZhox2cYCFNH8NMJ7pRg6AeSMPYAnDKECDg== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" -"@miniflare/durable-objects@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/durable-objects/-/durable-objects-2.14.1.tgz#48f29847afd7699e369fb0caa1a4fa23c1d39315" - integrity sha512-T+oHGw5GcEIilkzrf0xDES7jzLVqcXJzSGsEIWqnBFLtdlKmrZF679ulRLBbyMVgvpQz6FRONh9jTH1XIiuObQ== +"@miniflare/durable-objects@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/durable-objects/-/durable-objects-2.14.2.tgz#f94c86d0124e3476c67622add009a38c7f779552" + integrity sha512-BfK+ZkJABoi7gd/O6WbpsO4GrgW+0dmOBWJDlNBxQ7GIpa+w3n9+SNnrYUxKzWlPSvz+TfTTk381B1z/Z87lPw== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" - "@miniflare/storage-memory" "2.14.1" - undici "5.20.0" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" + "@miniflare/storage-memory" "2.14.2" + undici "5.28.2" -"@miniflare/html-rewriter@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/html-rewriter/-/html-rewriter-2.14.1.tgz#e4f109d5e0efe862717b1ff33e75644d538618b1" - integrity sha512-vp4uZXuEKhtIaxoXa7jgDAPItlzjbfoUqYWp+fwDKv4J4mfQnzzs/5hwjbE7+Ihm/KNI0zNi8P0sSWjIRFl6ng== +"@miniflare/html-rewriter@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/html-rewriter/-/html-rewriter-2.14.2.tgz#1f699255a1f4b76f5118bb35f0cd346127b61bce" + integrity sha512-tu0kd9bj38uZ04loHb3sMI8kzUzZPgPOAJEdS9zmdSPh0uOkjCDf/TEkKsDdv2OFysyb0DRsIrwhPqCTIrPf1Q== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" html-rewriter-wasm "^0.4.1" - undici "5.20.0" + undici "5.28.2" -"@miniflare/kv@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/kv/-/kv-2.14.1.tgz#6d04b7f11969baa1efc5c84d6c0ca0003afafa0a" - integrity sha512-Gp07Wcszle7ptsoO8mCtKQRs0AbQnYo1rgnxUcsTL3xJJaHXEA/B9EKSADS2XzJMeY4PgUOHU6Rf08OOF2yWag== +"@miniflare/kv@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/kv/-/kv-2.14.2.tgz#cbca1feb23338106b7c7a815bd4d38e08e6476b0" + integrity sha512-3rs4cJOGACT/U7NH7j4KD29ugXRYUiM0aGkvOEdFQtChXLsYClJljXpezTfJJxBwZjdS4F2UFTixtFcHp74UfA== dependencies: - "@miniflare/shared" "2.14.1" + "@miniflare/shared" "2.14.2" -"@miniflare/queues@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/queues/-/queues-2.14.1.tgz#ce586e08b5b4dbdae6c5ed0d76317336269554c0" - integrity sha512-uBzrbBkIgtNoztDpmMMISg/brYtxLHRE7oTaN8OVnq3bG+3nF9kQC42HUz+Vg+sf65UlvhSaqkjllgx+fNtOxQ== +"@miniflare/queues@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/queues/-/queues-2.14.2.tgz#c20b76948a720b257b748c5611b84623f6d17e55" + integrity sha512-OylkRs4lOWKvGnX+Azab3nx+1qwC87M36/hkgAU1RRvVDCOxOrYLvNLUczFfgmgMBwpYsmmW8YOIASlI3p4Qgw== dependencies: - "@miniflare/shared" "2.14.1" + "@miniflare/shared" "2.14.2" -"@miniflare/r2@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/r2/-/r2-2.14.1.tgz#57a352576746a6f7a19726e7559a6e5607c4afb9" - integrity sha512-grOMnGf2XSicbgxMYMBfWE37k/e7l5NnwXZIViQ+N06uksp+MLA8E6yKQNtvrWQS66TM8gBvMnWo96OFmYjb6Q== +"@miniflare/r2@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/r2/-/r2-2.14.2.tgz#7d970045c680c74859aa491180fdd6dfa187d39d" + integrity sha512-uuc7dx6OqSQT8i0F2rsigfizXmInssRvvJAjoi1ltaNZNJCHH9l1PwHfaNc/XAuDjYmiCjtHDaPdRvZU9g9F3g== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" - undici "5.20.0" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" + undici "5.28.2" -"@miniflare/runner-vm@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/runner-vm/-/runner-vm-2.14.1.tgz#401beb39f34151c7a12ce5018decb78767bfe5c2" - integrity sha512-UobsGM0ICVPDlJD54VPDSx0EXrIY3nJMXBy2zIFuuUOz4hQKXvMQ6jtAlJ8UNKer+XXI3Mb/9R/gfU8r6kxIMA== +"@miniflare/runner-vm@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/runner-vm/-/runner-vm-2.14.2.tgz#b108ee6ecdbdb84aaad94419b3be148df4cb3f18" + integrity sha512-WlyxAQ+bv/9Pm/xnbpgAg7RNX4pz/q3flytUoo4z4OrRmNEuXrbMUsJZnH8dziqzrZ2gCLkYIEzeaTmSQKp5Jg== dependencies: - "@miniflare/shared" "2.14.1" + "@miniflare/shared" "2.14.2" -"@miniflare/shared-test-environment@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/shared-test-environment/-/shared-test-environment-2.14.1.tgz#128ac33feba0ebe932f38a550e1b3f1207d396f1" - integrity sha512-hfactEWiHuHOmE29XFG8oLNCF6+HqjD6Mb80CzidcVmLlBTEtSC3PEF+DXPyvNdLXpBolZMKOuC/yzzloWvACA== +"@miniflare/shared-test-environment@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/shared-test-environment/-/shared-test-environment-2.14.2.tgz#a0a3a37c6d0c0fb94ab7e9bd56c8748651ff6091" + integrity sha512-97y/95EzDC86jM2bKYacKk4n59miFC+WXueRdW5TeVzBw2UTL2Alvy36AZuyMUgBqxZdJSv88/q0jHTw7LvyMA== dependencies: "@cloudflare/workers-types" "^4.20221111.1" - "@miniflare/cache" "2.14.1" - "@miniflare/core" "2.14.1" - "@miniflare/d1" "2.14.1" - "@miniflare/durable-objects" "2.14.1" - "@miniflare/html-rewriter" "2.14.1" - "@miniflare/kv" "2.14.1" - "@miniflare/queues" "2.14.1" - "@miniflare/r2" "2.14.1" - "@miniflare/shared" "2.14.1" - "@miniflare/sites" "2.14.1" - "@miniflare/storage-memory" "2.14.1" - "@miniflare/web-sockets" "2.14.1" - -"@miniflare/shared@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/shared/-/shared-2.14.1.tgz#b150787e2b846f43085b15747b5e6f8a90fa1634" - integrity sha512-73GnLtWn5iP936ctE6ZJrMqGu134KOoIIveq5Yd/B+NnbFfzpuzjCpkLrnqjkDdsxDbruXSb5eTR/SmAdpJxZQ== + "@miniflare/cache" "2.14.2" + "@miniflare/core" "2.14.2" + "@miniflare/d1" "2.14.2" + "@miniflare/durable-objects" "2.14.2" + "@miniflare/html-rewriter" "2.14.2" + "@miniflare/kv" "2.14.2" + "@miniflare/queues" "2.14.2" + "@miniflare/r2" "2.14.2" + "@miniflare/shared" "2.14.2" + "@miniflare/sites" "2.14.2" + "@miniflare/storage-memory" "2.14.2" + "@miniflare/web-sockets" "2.14.2" + +"@miniflare/shared@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/shared/-/shared-2.14.2.tgz#ee49f2989a18b30747415dd1f90da7ae851fea90" + integrity sha512-dDnYIztz10zDQjaFJ8Gy9UaaBWZkw3NyhFdpX6tAeyPA/2lGvkftc42MYmNi8s5ljqkZAtKgWAJnSf2K75NCJw== dependencies: "@types/better-sqlite3" "^7.6.0" kleur "^4.1.4" npx-import "^1.1.4" picomatch "^2.3.1" -"@miniflare/sites@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/sites/-/sites-2.14.1.tgz#f2bc8308c5488aea961d7a82ffe4f91d23411fb0" - integrity sha512-AbbIcU6VBeaNqVgMiLMWN2a09eX3jZmjaEi0uKqufVDqW/QIz47/30aC0O9qTe+XYpi3jjph/Ux7uEY8Z+enMw== +"@miniflare/sites@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/sites/-/sites-2.14.2.tgz#6b77f9cf0b6bf07f38c980e8e21c3dcfbd4fd867" + integrity sha512-jFOx1G5kD+kTubsga6jcFbMdU2nSuNG2/EkojwuhYT8hYp3qd8duvPyh1V+OR2tMvM4FWu6jXPXNZNBHXHQaUQ== dependencies: - "@miniflare/kv" "2.14.1" - "@miniflare/shared" "2.14.1" - "@miniflare/storage-file" "2.14.1" + "@miniflare/kv" "2.14.2" + "@miniflare/shared" "2.14.2" + "@miniflare/storage-file" "2.14.2" -"@miniflare/storage-file@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/storage-file/-/storage-file-2.14.1.tgz#4cf14baad1fee75d523a6cf7ea7bfabd6a7d7dd1" - integrity sha512-faZu9tRSW6c/looVFI/ZhkdGsIc9NfNCbSl3jJRmm7xgyZ+/S+dQ5JtGVbVsUIX8YGWDyE2j3oWCGCjxGLEpkg== +"@miniflare/storage-file@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/storage-file/-/storage-file-2.14.2.tgz#d180b9f1a32f92497ac5993b008e9eddfba938e6" + integrity sha512-tn8rqMBeTtN+ICHQAMKQ0quHGYIkcyDK0qKW+Ic14gdfGDZx45BqXExQM9wTVqKtwAt85zp5eKVUYQCFfUx46Q== dependencies: - "@miniflare/shared" "2.14.1" - "@miniflare/storage-memory" "2.14.1" + "@miniflare/shared" "2.14.2" + "@miniflare/storage-memory" "2.14.2" -"@miniflare/storage-memory@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/storage-memory/-/storage-memory-2.14.1.tgz#9615de975a85a25b3dccaa9f085c955550e364f2" - integrity sha512-lfQbQwopVWd4W5XzrYdp0rhk3dJpvSmv1Wwn9RhNO20WrcuoxpdSzbmpBahsgYVg+OheVaEbS6RpFqdmwwLTog== +"@miniflare/storage-memory@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/storage-memory/-/storage-memory-2.14.2.tgz#b4eac56ddeca8b839f10cce62914a7c843e44f70" + integrity sha512-9Wtz9mayHIY0LDsfpMGx+/sfKCq3eAQJzYY+ju1tTEaKR0sVAuO51wu0wbyldsjj9OcBcd2X32iPbIa7KcSZQQ== dependencies: - "@miniflare/shared" "2.14.1" + "@miniflare/shared" "2.14.2" -"@miniflare/watcher@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/watcher/-/watcher-2.14.1.tgz#2fe44470df44a1eff1313597ea2389c06a8f9b05" - integrity sha512-dkFvetm5wk6pwunlYb/UkI0yFNb3otLpRm5RDywMUzqObEf+rCiNNAbJe3HUspr2ncZVAaRWcEaDh82vYK5cmw== +"@miniflare/watcher@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/watcher/-/watcher-2.14.2.tgz#619f2f6dff630eddf9f690b611cc9669fdedb900" + integrity sha512-/TL0np4uYDl+6MdseDApZmDdlJ6Y7AY5iDY0TvUQJG9nyBoCjX6w0Zn4SiKDwO6660rPtSqZ5c7HzbPhGb5vsA== dependencies: - "@miniflare/shared" "2.14.1" + "@miniflare/shared" "2.14.2" -"@miniflare/web-sockets@2.14.1": - version "2.14.1" - resolved "https://registry.yarnpkg.com/@miniflare/web-sockets/-/web-sockets-2.14.1.tgz#d48ba704c32c19db7d0cfc88107e1d1e364e9b62" - integrity sha512-3N//L5EjF7+xXd7qCLR2ylUwm8t2MKyGPGWEtRBrQ2xqYYWhewKTjlquHCOPU5Irnnd/4BhTmFA55MNrq7m4Nw== +"@miniflare/web-sockets@2.14.2": + version "2.14.2" + resolved "https://registry.yarnpkg.com/@miniflare/web-sockets/-/web-sockets-2.14.2.tgz#8a7a1eead842aa96e0632382d0807c141a29037b" + integrity sha512-kpbVlznPuxNQahssQvZiNPQo/iPme7qV3WMQeg6TYNCkYD7n6vEqeFZ5E/eQgB59xCanpvw4Ci8y/+SdMK6BUg== dependencies: - "@miniflare/core" "2.14.1" - "@miniflare/shared" "2.14.1" - undici "5.20.0" + "@miniflare/core" "2.14.2" + "@miniflare/shared" "2.14.2" + undici "5.28.2" ws "^8.2.2" "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": @@ -6668,18 +6668,18 @@ jest-environment-jsdom@^29.5.0: jest-util "^29.7.0" jsdom "^20.0.0" -jest-environment-miniflare@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/jest-environment-miniflare/-/jest-environment-miniflare-2.14.1.tgz#45b63dfa00db73695c9760dd33b63545314384bc" - integrity sha512-IkyCJ7LJCIXE1xJaExPRVHTK+6RxFJYEQjaVnpMCn9gEXSnjZhFwxdD3uFJq3J9QtcuZKRFBKJurnmGFCV4otQ== +jest-environment-miniflare@^2.14.2: + version "2.14.2" + resolved "https://registry.yarnpkg.com/jest-environment-miniflare/-/jest-environment-miniflare-2.14.2.tgz#da6a5f408ce561db50dc5d442f4b9ca07600f5f9" + integrity sha512-ssczII1i1aRUsVRLGAijICq4vmFlrDJyOsnG7dqIkC/N9dQmusXA/A+x7TgEHnBGCVk7+JVS/QyZtJRbiXCUmQ== dependencies: "@jest/environment" ">=27" "@jest/fake-timers" ">=27" "@jest/types" ">=27" - "@miniflare/queues" "2.14.1" - "@miniflare/runner-vm" "2.14.1" - "@miniflare/shared" "2.14.1" - "@miniflare/shared-test-environment" "2.14.1" + "@miniflare/queues" "2.14.2" + "@miniflare/runner-vm" "2.14.2" + "@miniflare/shared" "2.14.2" + "@miniflare/shared-test-environment" "2.14.2" jest-mock ">=27" jest-util ">=27" @@ -9367,14 +9367,7 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici@5.20.0: - version "5.20.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" - integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== - dependencies: - busboy "^1.6.0" - -undici@^5.28.2: +undici@5.28.2, undici@^5.28.2: version "5.28.2" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==