import {compare} from 'compare-versions'; import '@shopify/shopify-api/adapters/node'; import { shopifyApi, ConfigParams as ApiConfigParams, Shopify, FeatureDeprecatedError, ShopifyError, ShopifyRestResources, Session, } from '@shopify/shopify-api'; import {MemorySessionStorage} from '@shopify/shopify-app-session-storage-memory'; import {SHOPIFY_EXPRESS_LIBRARY_VERSION} from './version'; import {AppConfigInterface, AppConfigParams} from './config-types'; import {IdempotentPromiseHandler} from './helpers/idempotent-promise-handler'; import {registerWebhooks} from './helpers/register-webhooks'; import {logDisabledFutureFlags} from './future/flags'; import { validateAuthenticatedSession, cspHeaders, ensureInstalled, redirectToShopifyOrAppRoot, } from './middlewares/index'; import {AuthMiddleware} from './auth/types'; import {auth} from './auth/index'; import {ProcessWebhooksMiddleware} from './webhooks/types'; import {processWebhooks} from './webhooks/index'; import { ValidateAuthenticatedSessionMiddleware, CspHeadersMiddleware, EnsureInstalledMiddleware, RedirectToShopifyOrAppRootMiddleware, } from './middlewares/types'; import {redirectOutOfApp} from './redirect-out-of-app'; import {RedirectOutOfAppFunction} from './types'; import {ensureValidOfflineSession} from './helpers/index'; export * from './types'; export * from './auth/types'; export * from './middlewares/types'; export * from './webhooks/types'; export type { AppConfigParams, ExpressApiConfigParams, FutureFlags, } from './config-types'; export {ApiVersion} from '@shopify/shopify-api'; type DefaultedConfigs | undefined> = ApiConfigParams & Params; type ConfigInterfaceFromParams< Params extends AppConfigParams | Omit, > = AppConfigInterface< Params extends AppConfigParams ? NonNullable['restResources']> : ShopifyRestResources, Params['sessionStorage'] extends undefined ? MemorySessionStorage : NonNullable >; export interface ShopifyApp { config: ConfigInterfaceFromParams; api: Shopify< DefaultedConfigs, DefaultedConfigs['restResources'] & ShopifyRestResources >; auth: AuthMiddleware; processWebhooks: ProcessWebhooksMiddleware; validateAuthenticatedSession: ValidateAuthenticatedSessionMiddleware; cspHeaders: CspHeadersMiddleware; ensureInstalledOnShop: EnsureInstalledMiddleware; redirectToShopifyOrAppRoot: RedirectToShopifyOrAppRootMiddleware; redirectOutOfApp: RedirectOutOfAppFunction; ensureValidOfflineSession: (shop: string) => Promise; registerWebhooks: (params: {session: Session}) => Promise; } export function shopifyApp( config: Params, ): ShopifyApp { const {api: apiConfig, ...appConfig} = config; const api = shopifyApi(apiConfigWithDefaults(apiConfig)); const validatedConfig = validateAppConfig(appConfig, api); if (validatedConfig.future?.tokenExchange && !api.config.isEmbeddedApp) { throw new ShopifyError( 'tokenExchange requires an embedded app (isEmbeddedApp: true). ' + 'Token exchange is not available for non-embedded apps; remove the flag or set isEmbeddedApp to true.', ); } logDisabledFutureFlags(validatedConfig, api.logger); return { config: validatedConfig, api: api as Shopify< DefaultedConfigs, DefaultedConfigs['restResources'] & ShopifyRestResources >, auth: auth({api, config: validatedConfig}), processWebhooks: processWebhooks({api, config: validatedConfig}), validateAuthenticatedSession: validateAuthenticatedSession({ api, config: validatedConfig, }), cspHeaders: cspHeaders({api}), ensureInstalledOnShop: ensureInstalled({ api, config: validatedConfig, }), redirectToShopifyOrAppRoot: redirectToShopifyOrAppRoot({ api, config: validatedConfig, }), redirectOutOfApp: redirectOutOfApp({api, config: validatedConfig}), ensureValidOfflineSession: (shop: string) => ensureValidOfflineSession({api, config: validatedConfig}, shop), registerWebhooks: ({session}: {session: Session}) => registerWebhooks(validatedConfig, api, session), }; } function apiConfigWithDefaults>( apiConfig: Params, ): DefaultedConfigs { let userAgent = `Shopify Express Library v${SHOPIFY_EXPRESS_LIBRARY_VERSION}`; if (apiConfig?.userAgentPrefix) { userAgent = `${apiConfig.userAgentPrefix} | ${userAgent}`; } /* eslint-disable no-process-env */ return { apiKey: process.env.SHOPIFY_API_KEY!, apiSecretKey: process.env.SHOPIFY_API_SECRET!, scopes: process.env.SCOPES?.split(',') as string[], hostScheme: process.env.HOST?.split('://')[0] as 'http' | 'https', hostName: process.env.HOST?.replace(/https?:\/\//, '') as string, isEmbeddedApp: true, ...apiConfig, userAgentPrefix: userAgent, } as DefaultedConfigs; /* eslint-enable no-process-env */ } function validateAppConfig>( config: Params, api: Shopify, ): ConfigInterfaceFromParams { const {sessionStorage, ...configWithoutSessionStorage} = config; return { // We override the API package's logger to add the right package context by default (and make the call simpler) logger: overrideLoggerPackage(api.logger), useOnlineTokens: false, exitIframePath: '/exitiframe', future: {}, hooks: {}, idempotentPromiseHandler: new IdempotentPromiseHandler(), sessionStorage: (sessionStorage ?? new MemorySessionStorage()) as ConfigInterfaceFromParams['sessionStorage'], ...configWithoutSessionStorage, auth: config.auth, webhooks: config.webhooks, }; } function overrideLoggerPackage(logger: Shopify['logger']): Shopify['logger'] { const baseContext = {package: 'shopify-app'}; const warningFunction: Shopify['logger']['warning'] = ( message, context = {}, ) => logger.warning(message, {...baseContext, ...context}); return { ...logger, log: (severity, message, context = {}) => logger.log(severity, message, {...baseContext, ...context}), debug: (message, context = {}) => logger.debug(message, {...baseContext, ...context}), info: (message, context = {}) => logger.info(message, {...baseContext, ...context}), warning: warningFunction, error: (message, context = {}) => logger.error(message, {...baseContext, ...context}), deprecated: deprecated(warningFunction), }; } function deprecated(warningFunction: Shopify['logger']['warning']) { return function (version: string, message: string): Promise { if (compare(SHOPIFY_EXPRESS_LIBRARY_VERSION, version, '>=')) { throw new FeatureDeprecatedError( `Feature was deprecated in version ${version}`, ); } return warningFunction(`[Deprecated | ${version}] ${message}`); }; }