import {adminClientFactory, storefrontClientFactory} from '../../../clients'; import {ensureValidOfflineSession} from '../../../helpers'; import {BasicParams} from '../../../types'; import { AppProxyContext, AppProxyContextWithSession, AuthenticateAppProxy, LiquidResponseFunction, } from './types'; export function authenticateAppProxyFactory( params: BasicParams, ): AuthenticateAppProxy { const {logger} = params; return async function authenticate( request: Request, ): Promise { const url = new URL(request.url); const shop = url.searchParams.get('shop')!; logger.info('Authenticating app proxy request', {shop}); if (!(await validateAppProxyHmac(params, url))) { logger.info('App proxy request has invalid signature', {shop}); throw new Response(undefined, { status: 400, statusText: 'Bad Request', }); } const session = await ensureValidOfflineSession(params, shop); if (!session) { logger.debug('Could not find offline session, returning empty context', { shop, ...Object.fromEntries(url.searchParams.entries()), }); const context: AppProxyContext = { liquid, session: undefined, admin: undefined, storefront: undefined, }; return context; } const context: AppProxyContextWithSession = { liquid, session, admin: adminClientFactory({params, session}), storefront: storefrontClientFactory({params, session}), }; return context; }; } const liquid: LiquidResponseFunction = (body, initAndOptions) => { const processedBody = processLiquidBody(body); if (typeof initAndOptions !== 'object') { return new Response(processedBody, { status: initAndOptions || 200, headers: { 'Content-Type': 'application/liquid', }, }); } const {layout, ...responseInit} = initAndOptions || {}; const responseBody = layout === false ? `{% layout none %} ${processedBody}` : processedBody; const headers = new Headers(responseInit.headers); headers.set('Content-Type', 'application/liquid'); return new Response(responseBody, { ...responseInit, headers, }); }; async function validateAppProxyHmac( params: BasicParams, url: URL, ): Promise { const {api, logger} = params; try { let searchParams = new URLSearchParams(url.search); if (!searchParams.get('index')) { searchParams.delete('index'); } let isValid = await api.utils.validateHmac( Object.fromEntries(searchParams.entries()), {signator: 'appProxy'}, ); if (!isValid) { const cleanPath = url.pathname .replace(/^\//, '') .replace(/\/$/, '') .replaceAll('/', '.'); const data = `routes%2F${cleanPath}`; searchParams = new URLSearchParams( `?_data=${data}&${searchParams.toString().replace(/^\?/, '')}`, ); isValid = await api.utils.validateHmac( Object.fromEntries(searchParams.entries()), {signator: 'appProxy'}, ); if (!isValid) { const searchParams = new URLSearchParams( `?_data=${data}._index&${url.search.replace(/^\?/, '')}`, ); isValid = await api.utils.validateHmac( Object.fromEntries(searchParams.entries()), {signator: 'appProxy'}, ); } } return isValid; } catch (error) { const shop = url.searchParams.get('shop')!; logger.info(error.message, {shop}); throw new Response(undefined, {status: 400, statusText: 'Bad Request'}); } } function processLiquidBody(body: string) { return ( body // Add trailing slashes to relative form action URLs .replaceAll( /<(form[^>]+)action="(\/[^"?]+)(\?[^"]+)?">/g, '<$1action="$2/$3">', ) // Add trailing slashes to relative link href URLs .replaceAll(/<(a[^>]+)href="(\/[^"?]+)(\?[^"]+)?">/g, '<$1href="$2/$3">') ); }