diff --git a/.changeset/angry-boats-kick.md b/.changeset/angry-boats-kick.md new file mode 100644 index 00000000..ce18f1a1 --- /dev/null +++ b/.changeset/angry-boats-kick.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": major +--- + +Changed publically exported paths. New exports will be documented in docs and migration guide diff --git a/.changeset/stale-flowers-serve.md b/.changeset/stale-flowers-serve.md new file mode 100644 index 00000000..6b8ebaf0 --- /dev/null +++ b/.changeset/stale-flowers-serve.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": major +--- + +Added new root exports: auth/browser and auth/node for token-related utilities diff --git a/package.json b/package.json index 820bcc05..fc0b2588 100644 --- a/package.json +++ b/package.json @@ -143,11 +143,6 @@ "import": "./settings-manager/index.mjs", "require": "./settings-manager/index.js" }, - "./urls": { - "types": "./urls.d.ts", - "import": "./urls.mjs", - "require": "./urls.js" - }, "./app-bridge": { "types": "./app-bridge/index.d.ts", "import": "./app-bridge/index.mjs", @@ -188,15 +183,10 @@ "import": "./saleor-app.mjs", "require": "./saleor-app.js" }, - "./verify-jwt": { - "types": "./verify-jwt.d.ts", - "import": "./verify-jwt.mjs", - "require": "./verify-jwt.js" - }, - "./verify-signature": { - "types": "./verify-signature.d.ts", - "import": "./verify-signature.mjs", - "require": "./verify-signature.js" + "./auth": { + "types": "./auth/index.d.ts", + "import": "./auth/index.mjs", + "require": "./auth/index.js" }, "./headers": { "types": "./headers.d.ts", @@ -204,9 +194,14 @@ "require": "./headers.js" }, "./util": { - "types": "./util/public/index.d.ts", - "import": "./util/public/index.mjs", - "require": "./util/public/index.js" + "types": "./util/index.d.ts", + "import": "./util/index.mjs", + "require": "./util/index.js" + }, + "./util/browser": { + "types": "./util/public/browser/index.d.ts", + "import": "./util/public/browser/index.mjs", + "require": "./util/public/browser/index.js" }, "./types": { "types": "./types.d.ts" diff --git a/src/app-bridge/app-bridge-provider.test.tsx b/src/app-bridge/app-bridge-provider.test.tsx index e62e472e..68fc99ca 100644 --- a/src/app-bridge/app-bridge-provider.test.tsx +++ b/src/app-bridge/app-bridge-provider.test.tsx @@ -39,7 +39,6 @@ describe("AppBridgeProvider", () => { { it("Returned instance provided in Provider", () => { const appBridge = new AppBridge({ - targetDomain: domain, + saleorApiUrl, }); const { result } = renderHook(() => useAppBridge(), { wrapper: (props: {}) => , }); - expect(result.current.appBridge?.getState().domain).toBe(domain); + expect(result.current.appBridge?.getState().saleorApiUrl).toBe(saleorApiUrl); }); it("Stores active state in React State", () => { const appBridge = new AppBridge({ - targetDomain: domain, saleorApiUrl, }); @@ -120,7 +118,6 @@ describe("useAppBridge hook", () => { return waitFor(() => { expect(renderCallback).toHaveBeenCalledTimes(2); expect(renderCallback).toHaveBeenCalledWith({ - domain, id: "appid", path: "", ready: false, diff --git a/src/app-bridge/app-bridge-state.test.ts b/src/app-bridge/app-bridge-state.test.ts index 69211ce1..2abb6b07 100644 --- a/src/app-bridge/app-bridge-state.test.ts +++ b/src/app-bridge/app-bridge-state.test.ts @@ -8,7 +8,6 @@ describe("app-bridge-state.ts", () => { expect(instance.getState()).toEqual({ id: "", - domain: "", ready: false, path: "/", theme: "light", @@ -21,7 +20,6 @@ describe("app-bridge-state.ts", () => { const instance = new AppBridgeStateContainer(); const newState: Partial = { - domain: "my-saleor-instance.cloud", saleorApiUrl: "https://my-saleor-instance.cloud/graphql/", id: "foo-bar", path: "/", @@ -42,7 +40,7 @@ describe("app-bridge-state.ts", () => { expect( new AppBridgeStateContainer({ initialLocale: "pl", - }).getState().locale + }).getState().locale, ).toBe("pl"); }); @@ -50,7 +48,7 @@ describe("app-bridge-state.ts", () => { expect( new AppBridgeStateContainer({ initialTheme: "dark", - }).getState().theme + }).getState().theme, ).toBe("dark"); }); }); diff --git a/src/app-bridge/app-bridge-state.ts b/src/app-bridge/app-bridge-state.ts index 03823b08..bc84b0d0 100644 --- a/src/app-bridge/app-bridge-state.ts +++ b/src/app-bridge/app-bridge-state.ts @@ -6,15 +6,10 @@ export type AppBridgeState = { token?: string; id: string; ready: boolean; - domain: string; path: string; theme: ThemeType; locale: LocaleCode; saleorApiUrl: string; - /** - * Versions of Saleor that app is mounted. Passed from the Dashboard. - * Works form Saleor 3.15 - */ saleorVersion?: string; dashboardVersion?: string; user?: { @@ -39,7 +34,6 @@ type Options = { export class AppBridgeStateContainer { private state: AppBridgeState = { id: "", - domain: "", saleorApiUrl: "", ready: false, path: "/", diff --git a/src/app-bridge/app-bridge.test.ts b/src/app-bridge/app-bridge.test.ts index 4694d59c..2dc3f9e1 100644 --- a/src/app-bridge/app-bridge.test.ts +++ b/src/app-bridge/app-bridge.test.ts @@ -65,7 +65,7 @@ const mockDashboardActionResponse = (actionType: ActionType, actionID: string) = payload: { ok: true, actionId: actionID }, } as DispatchResponseEvent, origin, - }) + }), ); } } @@ -96,10 +96,6 @@ describe("AppBridge", () => { vi.clearAllMocks(); }); - it("correctly sets the default domain, if not set in constructor", () => { - expect(appBridge.getState().domain).toEqual(domain); - }); - it("authenticates", () => { expect(appBridge.getState().ready).toBe(false); @@ -109,7 +105,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: { type: "handshake", payload: { token } }, origin, - }) + }), ); expect(appBridge.getState().ready).toBe(true); @@ -135,7 +131,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: handshakeEvent, origin, - }) + }), ); // incorrect event type @@ -144,7 +140,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: { type: "invalid", payload: { token: "invalid" } }, origin, - }) + }), ); // incorrect origin @@ -153,7 +149,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: { type: "handshake", payload: { token } }, origin: "http://wrong.origin.com", - }) + }), ); expect(callback).toHaveBeenCalledTimes(1); @@ -168,7 +164,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: { type: "handshake", payload: { token: validJwtToken } }, origin, - }) + }), ); expect(callback).toHaveBeenCalledTimes(1); @@ -188,7 +184,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: themeEvent, origin, - }) + }), ); expect(callback).toHaveBeenCalledOnce(); @@ -197,10 +193,6 @@ describe("AppBridge", () => { unsubscribe(); }); - it("persists domain", () => { - expect(appBridge.getState().domain).toEqual(domain); - }); - it("dispatches valid action", () => { const target = "/test"; const action = actions.Redirect({ to: target }); @@ -225,7 +217,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: handshakeEvent, origin, - }) + }), ); expect(cb1).toHaveBeenCalledTimes(1); @@ -238,21 +230,13 @@ describe("AppBridge", () => { new MessageEvent("message", { data: handshakeEvent, origin, - }) + }), ); expect(cb1).toHaveBeenCalledTimes(1); expect(cb2).toHaveBeenCalledTimes(1); }); - it("attaches domain from options in constructor", () => { - appBridge = new AppBridge({ - targetDomain: "https://foo.bar", - }); - - expect(appBridge.getState().domain).toEqual("https://foo.bar"); - }); - it.each(["pl", "en", "it"])("sets initial locale \"%s\" from constructor", (locale) => { const instance = new AppBridge({ initialLocale: locale, @@ -306,7 +290,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: handshakeEvent, origin, - }) + }), ); expect(appBridge.getState().token).toEqual(handshakeEvent.payload.token); @@ -316,7 +300,7 @@ describe("AppBridge", () => { new MessageEvent("message", { data: tokenRefreshEvent, origin, - }) + }), ); expect(appBridge.getState().token).toEqual(tokenRefreshEvent.payload.token); @@ -334,7 +318,7 @@ describe("AppBridge", () => { dashboard: "3.15.1", }), origin, - }) + }), ); expect(appBridge.getState().token).toEqual(validJwtToken); diff --git a/src/app-bridge/app-bridge.ts b/src/app-bridge/app-bridge.ts index 8c31f9a4..7e5bf010 100644 --- a/src/app-bridge/app-bridge.ts +++ b/src/app-bridge/app-bridge.ts @@ -85,7 +85,6 @@ const createEmptySubscribeMap = (): SubscribeMap => ({ }); export type AppBridgeOptions = { - targetDomain?: string; saleorApiUrl?: string; initialLocale?: LocaleCode; /** @@ -103,12 +102,6 @@ const getLocaleFromUrl = () => (new URL(window.location.href).searchParams.get(AppIframeParams.LOCALE) as LocaleCode) || undefined; -/** - * TODO: Probably remove empty string fallback - */ -const getDomainFromUrl = () => - new URL(window.location.href).searchParams.get(AppIframeParams.DOMAIN) || ""; - const getSaleorApiUrlFromUrl = () => new URL(window.location.href).searchParams.get(AppIframeParams.SALEOR_API_URL) || ""; @@ -125,7 +118,6 @@ const getThemeFromUrl = () => { }; const getDefaultOptions = (): AppBridgeOptions => ({ - targetDomain: getDomainFromUrl(), saleorApiUrl: getSaleorApiUrlFromUrl(), initialLocale: getLocaleFromUrl() ?? "en", autoNotifyReady: true, @@ -146,7 +138,7 @@ export class AppBridge { if (SSR) { throw new Error( - "AppBridge detected you're running this app in SSR mode. Make sure to call `new AppBridge()` when window object exists." + "AppBridge detected you're running this app in SSR mode. Make sure to call `new AppBridge()` when window object exists.", ); } @@ -170,14 +162,8 @@ export class AppBridge { debug("?saleorApiUrl was not found in iframe url"); } - if (!this.combinedOptions.targetDomain) { - debug("?domain was not found in iframe url"); - } - - if (!(this.combinedOptions.saleorApiUrl || this.combinedOptions.targetDomain)) { - console.error( - "domain and saleorApiUrl params were not found in iframe url. Ensure at least one of them is present" - ); + if (!this.combinedOptions.saleorApiUrl) { + console.error("saleorApiUrl param was not found in iframe url"); } this.setInitialState(); @@ -197,7 +183,7 @@ export class AppBridge { */ subscribe>( eventType: TEventType, - cb: EventCallback + cb: EventCallback, ) { debug("subscribe() called with event %s and callback %s", eventType, cb.name); @@ -251,7 +237,7 @@ export class AppBridge { type: action.type, payload: action.payload, }, - "*" + "*", ); let timeoutId: number; @@ -261,7 +247,7 @@ export class AppBridge { "Subscribing to %s with action id: %s and status 'ok' is: %s", EventType.response, actionId, - ok + ok, ); if (action.payload.actionId === actionId) { @@ -274,8 +260,8 @@ export class AppBridge { } else { reject( new Error( - "Action responded with negative status. This indicates the action method was not used properly." - ) + "Action responded with negative status. This indicates the action method was not used properly.", + ), ); } } @@ -312,7 +298,6 @@ export class AppBridge { const path = window.location.pathname || ""; const state: Partial = { - domain: this.combinedOptions.targetDomain, id, path, theme: this.combinedOptions.initialTheme, @@ -355,7 +340,7 @@ export class AppBridge { this.subscribeMap[type][key](payload); }); } - } + }, ); } } diff --git a/src/app-bridge/fetch.test.ts b/src/app-bridge/fetch.test.ts index 5602c147..2adab660 100644 --- a/src/app-bridge/fetch.test.ts +++ b/src/app-bridge/fetch.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { SALEOR_AUTHORIZATION_BEARER_HEADER } from "../const"; +import { SALEOR_AUTHORIZATION_BEARER_HEADER } from "@/headers"; + import { AppBridge } from "./app-bridge"; import { AppBridgeState } from "./app-bridge-state"; import { createAuthenticatedFetch } from "./fetch"; @@ -9,7 +10,6 @@ describe("createAuthenticatedFetch", () => { const mockedAppBridge: Pick = { getState(): AppBridgeState { return { - domain: "master.staging.saleor.cloud", token: "XXX_YYY", locale: "pl", path: "/", diff --git a/src/app-bridge/fetch.ts b/src/app-bridge/fetch.ts index 1f0d7a96..de856521 100644 --- a/src/app-bridge/fetch.ts +++ b/src/app-bridge/fetch.ts @@ -1,6 +1,7 @@ import { useMemo } from "react"; -import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "../const"; +import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@/headers"; + import { AppBridge } from "./app-bridge"; import { useAppBridge } from "./app-bridge-provider"; diff --git a/src/app-bridge/index.ts b/src/app-bridge/index.ts index c3a1e013..ddb56733 100644 --- a/src/app-bridge/index.ts +++ b/src/app-bridge/index.ts @@ -10,10 +10,3 @@ export * from "./fetch"; export * from "./types"; export * from "./use-dashboard-token"; export * from "./with-authorization"; - -/** - * @deprecated use new AppBridge(), createApp will be removed - */ -export const createApp = (targetDomain?: string) => - new AppBridge(targetDomain ? { targetDomain } : undefined); -export default createApp; diff --git a/src/app-bridge/with-authorization.tsx b/src/app-bridge/with-authorization.tsx index eeae84e8..378b8a9d 100644 --- a/src/app-bridge/with-authorization.tsx +++ b/src/app-bridge/with-authorization.tsx @@ -2,7 +2,9 @@ import { NextPage } from "next"; import * as React from "react"; import { PropsWithChildren, ReactNode } from "react"; -import { isInIframe, useIsMounted } from "../util"; +import { isInIframe } from "@/util/is-in-iframe"; +import { useIsMounted } from "@/util/use-is-mounted"; + import { useDashboardToken } from "./use-dashboard-token"; function SimpleError({ children }: PropsWithChildren<{}>) { @@ -41,7 +43,7 @@ type WithAuthorizationHOC

= React.FunctionComponent

& { export const withAuthorization = (props: Props = defaultProps) => >( - BaseComponent: React.FunctionComponent + BaseComponent: React.FunctionComponent, ): WithAuthorizationHOC => { const { dashboardTokenInvalid, noDashboardToken, notIframe, unmounted } = { ...defaultProps, diff --git a/src/fetch-remote-jwks.ts b/src/auth/fetch-remote-jwks.ts similarity index 85% rename from src/fetch-remote-jwks.ts rename to src/auth/fetch-remote-jwks.ts index 85fcf6c0..3fe3814e 100644 --- a/src/fetch-remote-jwks.ts +++ b/src/auth/fetch-remote-jwks.ts @@ -1,8 +1,9 @@ import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { SemanticAttributes } from "@opentelemetry/semantic-conventions"; -import { getOtelTracer, OTEL_CORE_SERVICE_NAME } from "./open-telemetry"; -import { getJwksUrlFromSaleorApiUrl } from "./urls"; +import { getJwksUrlFromSaleorApiUrl } from "@/auth/index"; + +import { getOtelTracer, OTEL_CORE_SERVICE_NAME } from "../open-telemetry"; export const fetchRemoteJwks = async (saleorApiUrl: string) => { const tracer = getOtelTracer(); @@ -31,6 +32,6 @@ export const fetchRemoteJwks = async (saleorApiUrl: string) => { } finally { span.end(); } - } + }, ); }; diff --git a/src/has-permissions-in-jwt-token.ts b/src/auth/has-permissions-in-jwt-token.ts similarity index 73% rename from src/has-permissions-in-jwt-token.ts rename to src/auth/has-permissions-in-jwt-token.ts index 0aba37fa..8a69ddbc 100644 --- a/src/has-permissions-in-jwt-token.ts +++ b/src/auth/has-permissions-in-jwt-token.ts @@ -1,12 +1,16 @@ -import { createDebug } from "./debug"; -import { Permission } from "./types"; +import { createDebug } from "../debug"; +import { Permission } from "../types"; import { DashboardTokenPayload } from "./verify-jwt"; const debug = createDebug("checkJwtPermissions"); +/** + * Takes decoded JWT token that Dashboard provides via AppBridge. + * Compare permissions against required in parameter + */ export const hasPermissionsInJwtToken = ( tokenData?: Pick, - permissionsToCheckAgainst?: Permission[] + permissionsToCheckAgainst?: Permission[], ) => { debug(`Permissions required ${permissionsToCheckAgainst}`); @@ -23,7 +27,7 @@ export const hasPermissionsInJwtToken = ( } const arePermissionsSatisfied = permissionsToCheckAgainst.every((permission) => - userPermissions.includes(permission) + userPermissions.includes(permission), ); if (!arePermissionsSatisfied) { diff --git a/src/has-permissions.in-jwt-token.test.ts b/src/auth/has-permissions.in-jwt-token.test.ts similarity index 96% rename from src/has-permissions.in-jwt-token.test.ts rename to src/auth/has-permissions.in-jwt-token.test.ts index 238097c9..5b57bb70 100644 --- a/src/has-permissions.in-jwt-token.test.ts +++ b/src/auth/has-permissions.in-jwt-token.test.ts @@ -23,7 +23,7 @@ describe("hasPermissionsInJwtToken", () => { user_permissions: ["MANAGE_ORDERS", "MANAGE_CHECKOUTS", "HANDLE_TAXES"], }; await expect( - hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]) + hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]), ).toBeTruthy(); }); @@ -32,7 +32,7 @@ describe("hasPermissionsInJwtToken", () => { user_permissions: ["MANAGE_ORDERS", "HANDLE_TAXES"], }; await expect( - hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]) + hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]), ).toBeFalsy(); }); @@ -41,7 +41,7 @@ describe("hasPermissionsInJwtToken", () => { user_permissions: [], }; await expect( - hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]) + hasPermissionsInJwtToken(tokenData, ["MANAGE_ORDERS", "MANAGE_CHECKOUTS"]), ).toBeFalsy(); }); }); diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 00000000..f4764e29 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,2 @@ +export { verifyJWT } from "./verify-jwt"; +export { getJwksUrlFromSaleorApiUrl, verifySignatureWithJwks } from "./verify-signature"; diff --git a/src/verify-jwt.test.ts b/src/auth/verify-jwt.test.ts similarity index 97% rename from src/verify-jwt.test.ts rename to src/auth/verify-jwt.test.ts index aebe94c1..ad1fb8e3 100644 --- a/src/verify-jwt.test.ts +++ b/src/auth/verify-jwt.test.ts @@ -42,13 +42,13 @@ describe("verifyJWT", () => { it("Throw error on decode issue", async () => { await expect( - verifyJWT({ appId: validAppId, saleorApiUrl: validApiUrl, token: "wrong_token" }) + verifyJWT({ appId: validAppId, saleorApiUrl: validApiUrl, token: "wrong_token" }), ).rejects.toThrow("JWT verification failed: Could not decode authorization token."); }); it("Throw error on app ID missmatch", async () => { await expect( - verifyJWT({ appId: "wrong_id", saleorApiUrl: validApiUrl, token: validToken }) + verifyJWT({ appId: "wrong_id", saleorApiUrl: validApiUrl, token: validToken }), ).rejects.toThrow("JWT verification failed: Token's app property is different than app ID."); }); @@ -59,7 +59,7 @@ describe("verifyJWT", () => { saleorApiUrl: validApiUrl, token: validToken, requiredPermissions: ["HANDLE_TAXES"], - }) + }), ).rejects.toThrow("JWT verification failed: Token's permissions are not sufficient."); }); @@ -71,7 +71,7 @@ describe("verifyJWT", () => { appId: validAppId, saleorApiUrl: validApiUrl, token: validToken, - }) + }), ).rejects.toThrow("JWT verification failed: Token is expired"); }); }); diff --git a/src/verify-jwt.ts b/src/auth/verify-jwt.ts similarity index 91% rename from src/verify-jwt.ts rename to src/auth/verify-jwt.ts index e9f5a4d2..b365460c 100644 --- a/src/verify-jwt.ts +++ b/src/auth/verify-jwt.ts @@ -1,9 +1,10 @@ import * as jose from "jose"; -import { createDebug } from "./debug"; +import { getJwksUrlFromSaleorApiUrl } from "@/auth/index"; + +import { createDebug } from "../debug"; +import { Permission } from "../types"; import { hasPermissionsInJwtToken } from "./has-permissions-in-jwt-token"; -import { Permission } from "./types"; -import { getJwksUrlFromSaleorApiUrl } from "./urls"; import { verifyTokenExpiration } from "./verify-token-expiration"; const debug = createDebug("verify-jwt"); @@ -45,7 +46,7 @@ export const verifyJWT = async ({ if (tokenClaims.app !== appId) { debug( - "Resolved App ID value from token to be different than in request, will respond with Bad Request" + "Resolved App ID value from token to be different than in request, will respond with Bad Request", ); throw new Error(`${ERROR_MESSAGE} Token's app property is different than app ID.`); diff --git a/src/verify-signature.ts b/src/auth/verify-signature.ts similarity index 52% rename from src/verify-signature.ts rename to src/auth/verify-signature.ts index 78f7f411..0bd966a8 100644 --- a/src/verify-signature.ts +++ b/src/auth/verify-signature.ts @@ -1,45 +1,9 @@ import * as jose from "jose"; -import { createDebug } from "./debug"; -import { getJwksUrlFromSaleorApiUrl } from "./urls"; +import { createDebug } from "../debug"; const debug = createDebug("verify-signature"); -/** - * Verify Webhook payload signature with public key of given `domain` - * https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#payload-signature - * - * Use Saleor URL to fetch JWKS - * - * TODO: Add test - */ -export const verifySignatureFromApiUrl = async ( - saleorApiUrl: string, - signature: string, - rawBody: string -) => { - const [header, , jwsSignature] = signature.split("."); - const jws: jose.FlattenedJWSInput = { - protected: header, - payload: rawBody, - signature: jwsSignature, - }; - - const remoteJwks = jose.createRemoteJWKSet( - new URL(getJwksUrlFromSaleorApiUrl(saleorApiUrl)) - ) as jose.FlattenedVerifyGetKey; - - debug("Created remote JWKS"); - - try { - await jose.flattenedVerify(jws, remoteJwks); - debug("JWKS verified"); - } catch { - debug("JWKS verification failed"); - throw new Error("JWKS verification failed"); - } -}; - /** * Verify the Webhook payload signature from provided JWKS string. * JWKS can be cached to avoid unnecessary calls. @@ -74,3 +38,6 @@ export const verifySignatureWithJwks = async (jwks: string, signature: string, r throw new Error("JWKS verification failed"); } }; + +export const getJwksUrlFromSaleorApiUrl = (saleorApiUrl: string): string => + `${new URL(saleorApiUrl).origin}/.well-known/jwks.json`; diff --git a/src/verify-token-expiration.test.ts b/src/auth/verify-token-expiration.test.ts similarity index 93% rename from src/verify-token-expiration.test.ts rename to src/auth/verify-token-expiration.test.ts index 207644af..c427baca 100644 --- a/src/verify-token-expiration.test.ts +++ b/src/auth/verify-token-expiration.test.ts @@ -27,7 +27,7 @@ describe("verifyTokenExpiration", () => { * Must be seconds */ exp: tokenDate.valueOf() / 1000, - } as DashboardTokenPayload) + } as DashboardTokenPayload), ).not.toThrow(); }); @@ -44,7 +44,7 @@ describe("verifyTokenExpiration", () => { * Must be seconds */ exp: tokenDate.valueOf() / 1000, - } as DashboardTokenPayload) + } as DashboardTokenPayload), ).toThrow(); }); @@ -55,7 +55,7 @@ describe("verifyTokenExpiration", () => { * Must be seconds */ exp: mockTodayDate.valueOf() / 1000, - } as DashboardTokenPayload) + } as DashboardTokenPayload), ).toThrow(); }); }); diff --git a/src/verify-token-expiration.ts b/src/auth/verify-token-expiration.ts similarity index 69% rename from src/verify-token-expiration.ts rename to src/auth/verify-token-expiration.ts index 7e670d63..e7da5986 100644 --- a/src/verify-token-expiration.ts +++ b/src/auth/verify-token-expiration.ts @@ -1,8 +1,12 @@ -import { createDebug } from "./debug"; +import { createDebug } from "../debug"; import { DashboardTokenPayload } from "./verify-jwt"; const debug = createDebug("verify-token-expiration"); +/** + * Takes user token that Dashboard provides via AppBridge (decoded). + * Checks token expiration and throws if expired + */ export const verifyTokenExpiration = (token: DashboardTokenPayload) => { const tokenExpiration = token.exp; const now = new Date(); @@ -18,9 +22,9 @@ export const verifyTokenExpiration = (token: DashboardTokenPayload) => { const tokenMsTimestamp = tokenExpiration * 1000; debug( - "Comparing todays date: %s and token expiration date: %s", + "Comparing to days date: %s and token expiration date: %s", now.toLocaleString(), - new Date(tokenMsTimestamp).toLocaleString() + new Date(tokenMsTimestamp).toLocaleString(), ); if (tokenMsTimestamp <= nowTimestamp) { diff --git a/src/const.ts b/src/const.ts deleted file mode 100644 index d50a0d25..00000000 --- a/src/const.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const SALEOR_EVENT_HEADER = "saleor-event"; -export const SALEOR_SIGNATURE_HEADER = "saleor-signature"; -export const SALEOR_AUTHORIZATION_BEARER_HEADER = "authorization-bearer"; -export const SALEOR_API_URL_HEADER = "saleor-api-url"; -export const SALEOR_SCHEMA_VERSION = "saleor-schema-version"; - -export * from "./locales"; diff --git a/src/handlers/actions/manifest-action-handler.test.ts b/src/handlers/actions/manifest-action-handler.test.ts index 99cfd3f8..a34ad7bc 100644 --- a/src/handlers/actions/manifest-action-handler.test.ts +++ b/src/handlers/actions/manifest-action-handler.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { SALEOR_SCHEMA_VERSION } from "@/const"; +import { SALEOR_SCHEMA_VERSION_HEADER } from "@/headers"; import { MockAdapter } from "@/test-utils/mock-adapter"; import { AppManifest } from "@/types"; @@ -21,7 +21,7 @@ describe("ManifestActionHandler", () => { beforeEach(() => { adapter = new MockAdapter({ mockHeaders: { - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, baseUrl: "http://example.com", }); diff --git a/src/handlers/actions/manifest-action-handler.ts b/src/handlers/actions/manifest-action-handler.ts index 34ad0dd3..78ecd123 100644 --- a/src/handlers/actions/manifest-action-handler.ts +++ b/src/handlers/actions/manifest-action-handler.ts @@ -1,6 +1,6 @@ import { createDebug } from "@/debug"; import { AppManifest, SaleorSchemaVersion } from "@/types"; -import { parseSchemaVersion } from "@/util"; +import { parseSchemaVersion } from "@/util/schema-version"; import { ActionHandlerInterface, diff --git a/src/handlers/actions/register-action-handler.test.ts b/src/handlers/actions/register-action-handler.test.ts index 70eec0d1..9f87e7c6 100644 --- a/src/handlers/actions/register-action-handler.test.ts +++ b/src/handlers/actions/register-action-handler.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { SALEOR_API_URL_HEADER } from "@/const"; -import * as fetchRemoteJwksModule from "@/fetch-remote-jwks"; +import * as fetchRemoteJwksModule from "@/auth/fetch-remote-jwks"; import * as getAppIdModule from "@/get-app-id"; +import { SALEOR_API_URL_HEADER } from "@/headers"; import { MockAdapter } from "@/test-utils/mock-adapter"; import { MockAPL } from "@/test-utils/mock-apl"; diff --git a/src/handlers/actions/register-action-handler.ts b/src/handlers/actions/register-action-handler.ts index be9e7828..e747b8bb 100644 --- a/src/handlers/actions/register-action-handler.ts +++ b/src/handlers/actions/register-action-handler.ts @@ -1,9 +1,9 @@ /* eslint-disable max-classes-per-file */ import { APL, AuthData } from "@/APL"; -import { SALEOR_API_URL_HEADER } from "@/const"; +import { fetchRemoteJwks } from "@/auth/fetch-remote-jwks"; import { createDebug } from "@/debug"; -import { fetchRemoteJwks } from "@/fetch-remote-jwks"; import { getAppId } from "@/get-app-id"; +import { SALEOR_API_URL_HEADER } from "@/headers"; import { GenericCreateAppRegisterHandlerOptions } from "../shared"; import { diff --git a/src/handlers/platforms/aws-lambda/create-app-register-handler.test.ts b/src/handlers/platforms/aws-lambda/create-app-register-handler.test.ts index 0d73b977..c6160b2f 100644 --- a/src/handlers/platforms/aws-lambda/create-app-register-handler.test.ts +++ b/src/handlers/platforms/aws-lambda/create-app-register-handler.test.ts @@ -2,9 +2,9 @@ import type { APIGatewayProxyEventV2 } from "aws-lambda"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AuthData } from "@/APL"; -import { SALEOR_API_URL_HEADER } from "@/const"; -import * as fetchRemoteJwksModule from "@/fetch-remote-jwks"; +import * as fetchRemoteJwksModule from "@/auth/fetch-remote-jwks"; import * as getAppIdModule from "@/get-app-id"; +import { SALEOR_API_URL_HEADER } from "@/headers"; import { MockAPL } from "@/test-utils/mock-apl"; import { @@ -103,19 +103,19 @@ describe("AWS Lambda createAppRegisterHandler", () => { expect.objectContaining({ authToken, saleorApiUrl, - }) + }), ); expect(mockOnRequestVerified).toHaveBeenCalledWith( event, expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplSaved).toHaveBeenCalledWith( event, expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplFailed).not.toHaveBeenCalled(); }); @@ -139,7 +139,7 @@ describe("AWS Lambda createAppRegisterHandler", () => { expect.objectContaining({ error: expect.any(Error), authData: expectedAuthData, - }) + }), ); }); @@ -150,7 +150,7 @@ describe("AWS Lambda createAppRegisterHandler", () => { context.respondWithError({ status: 401, message: "test message", - }) + }), ); const handler = createAppRegisterHandler({ apl: mockApl, diff --git a/src/handlers/platforms/aws-lambda/create-manifest-handler.test.ts b/src/handlers/platforms/aws-lambda/create-manifest-handler.test.ts index 67ac0b60..9098353d 100644 --- a/src/handlers/platforms/aws-lambda/create-manifest-handler.test.ts +++ b/src/handlers/platforms/aws-lambda/create-manifest-handler.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { SALEOR_SCHEMA_VERSION } from "@/const"; +import { SALEOR_SCHEMA_VERSION_HEADER } from "@/headers"; import { createManifestHandler, CreateManifestHandlerOptions } from "./create-manifest-handler"; import { createLambdaEvent, mockLambdaContext } from "./test-utils"; @@ -17,7 +17,7 @@ describe("AWS Lambda createManifestHandler", () => { "content-type": "application/json", host: "some-app-host.cloud", "x-forwarded-proto": "https", - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, }); const expectedBaseUrl = "https://some-app-host.cloud"; @@ -69,7 +69,7 @@ describe("AWS Lambda createManifestHandler", () => { "content-type": "application/json", host: "some-app-host.cloud", "x-forwarded-proto": "https", - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, requestContext: { stage: "test", diff --git a/src/handlers/platforms/fetch-api/create-app-register-handler.test.ts b/src/handlers/platforms/fetch-api/create-app-register-handler.test.ts index b3cf49cf..369d89a7 100644 --- a/src/handlers/platforms/fetch-api/create-app-register-handler.test.ts +++ b/src/handlers/platforms/fetch-api/create-app-register-handler.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { AuthData } from "@/APL"; -import { SALEOR_API_URL_HEADER } from "@/const"; -import * as fetchRemoteJwksModule from "@/fetch-remote-jwks"; +import * as fetchRemoteJwksModule from "@/auth/fetch-remote-jwks"; import * as getAppIdModule from "@/get-app-id"; +import { SALEOR_API_URL_HEADER } from "@/headers"; import { MockAPL } from "@/test-utils/mock-apl"; import { @@ -114,19 +114,19 @@ describe("Fetch API createAppRegisterHandler", () => { expect.objectContaining({ authToken, saleorApiUrl, - }) + }), ); expect(mockOnRequestVerified).toHaveBeenCalledWith( request, expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplSaved).toHaveBeenCalledWith( request, expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplFailed).not.toHaveBeenCalled(); }); @@ -150,7 +150,7 @@ describe("Fetch API createAppRegisterHandler", () => { expect.objectContaining({ error: expect.any(Error), authData: expectedAuthData, - }) + }), ); }); @@ -161,7 +161,7 @@ describe("Fetch API createAppRegisterHandler", () => { context.respondWithError({ status: 401, message: "test message", - }) + }), ); const handler = createAppRegisterHandler({ apl: mockApl, diff --git a/src/handlers/platforms/fetch-api/create-app-register-handler.ts b/src/handlers/platforms/fetch-api/create-app-register-handler.ts index a3625977..93143900 100644 --- a/src/handlers/platforms/fetch-api/create-app-register-handler.ts +++ b/src/handlers/platforms/fetch-api/create-app-register-handler.ts @@ -29,7 +29,7 @@ export type CreateAppRegisterHandlerOptions = export const createAppRegisterHandler = (config: CreateAppRegisterHandlerOptions): WebApiHandler => async (req) => { - const adapter = new WebApiAdapter(req); + const adapter = new WebApiAdapter(req, Response); const useCase = new RegisterActionHandler(adapter); const result = await useCase.handleAction(config); return adapter.send(result); diff --git a/src/handlers/platforms/fetch-api/create-manifest-handler.test.ts b/src/handlers/platforms/fetch-api/create-manifest-handler.test.ts index 39d18b86..7e7101a5 100644 --- a/src/handlers/platforms/fetch-api/create-manifest-handler.test.ts +++ b/src/handlers/platforms/fetch-api/create-manifest-handler.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { SALEOR_SCHEMA_VERSION } from "@/const"; +import { SALEOR_SCHEMA_VERSION_HEADER } from "@/headers"; import { createManifestHandler, CreateManifestHandlerOptions } from "./create-manifest-handler"; @@ -11,7 +11,7 @@ describe("Fetch API createManifestHandler", () => { headers: { host: "some-app-host.cloud", "x-forwarded-proto": "https", - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, method: "GET", }); diff --git a/src/handlers/platforms/fetch-api/create-manifest-handler.ts b/src/handlers/platforms/fetch-api/create-manifest-handler.ts index 828157e6..c0e45b86 100644 --- a/src/handlers/platforms/fetch-api/create-manifest-handler.ts +++ b/src/handlers/platforms/fetch-api/create-manifest-handler.ts @@ -27,7 +27,7 @@ export type CreateManifestHandlerOptions = GenericHandlerOptions async (request: Request) => { - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const actionHandler = new ManifestActionHandler(adapter); const result = await actionHandler.handleAction(config); return adapter.send(result); diff --git a/src/handlers/platforms/fetch-api/create-protected-handler.ts b/src/handlers/platforms/fetch-api/create-protected-handler.ts index e360250a..d17a1534 100644 --- a/src/handlers/platforms/fetch-api/create-protected-handler.ts +++ b/src/handlers/platforms/fetch-api/create-protected-handler.ts @@ -9,17 +9,17 @@ import { WebApiAdapter, WebApiHandler } from "./platform-adapter"; export type WebApiProtectedHandler = ( request: Request, - ctx: ProtectedHandlerContext + ctx: ProtectedHandlerContext, ) => Response | Promise; export const createProtectedHandler = ( handlerFn: WebApiProtectedHandler, apl: APL, - requiredPermissions?: Permission[] + requiredPermissions?: Permission[], ): WebApiHandler => async (request) => { - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const actionValidator = new ProtectedActionValidator(adapter); const validationResult = await actionValidator.validateRequest({ apl, diff --git a/src/handlers/platforms/fetch-api/index.ts b/src/handlers/platforms/fetch-api/index.ts index b1ad0cd4..62bb8ecd 100644 --- a/src/handlers/platforms/fetch-api/index.ts +++ b/src/handlers/platforms/fetch-api/index.ts @@ -4,3 +4,4 @@ export * from "./create-protected-handler"; export * from "./platform-adapter"; export * from "./saleor-webhooks/saleor-async-webhook"; export * from "./saleor-webhooks/saleor-sync-webhook"; +export { WebApiWebhookHandler } from "./saleor-webhooks/saleor-webhook"; diff --git a/src/handlers/platforms/fetch-api/platform-adapter.test.ts b/src/handlers/platforms/fetch-api/platform-adapter.test.ts index e144b3f8..265946f3 100644 --- a/src/handlers/platforms/fetch-api/platform-adapter.test.ts +++ b/src/handlers/platforms/fetch-api/platform-adapter.test.ts @@ -15,7 +15,7 @@ describe("WebApiAdapter", () => { headers, body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getHeader("host")).toBe("example.com"); expect(adapter.getHeader("non-existent")).toBeNull(); @@ -30,7 +30,7 @@ describe("WebApiAdapter", () => { headers, body: JSON.stringify(sampleJson), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const body = await adapter.getBody(); expect(body).toEqual(sampleJson); }); @@ -41,7 +41,7 @@ describe("WebApiAdapter", () => { headers, body: "{ ", // invalid JSON }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); await expect(adapter.getBody()).rejects.toThrowError(); }); @@ -51,7 +51,7 @@ describe("WebApiAdapter", () => { headers, body: "{}", }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); await adapter.getBody(); expect(request.bodyUsed).toBe(false); @@ -70,7 +70,7 @@ describe("WebApiAdapter", () => { }), body: "plain text", }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const body = await adapter.getRawBody(); expect(body).toBe("plain text"); }); @@ -84,7 +84,7 @@ describe("WebApiAdapter", () => { }), body: "plain text", }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); await adapter.getRawBody(); expect(request.bodyUsed).toBe(false); @@ -103,7 +103,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getBaseUrl()).toBe("https://example.com"); }); @@ -116,7 +116,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getBaseUrl()).toBe("https://example.com"); }); @@ -129,7 +129,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getBaseUrl()).toBe("http://example.com"); }); @@ -142,7 +142,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getBaseUrl()).toBe("wss://example.com"); }); @@ -153,7 +153,7 @@ describe("WebApiAdapter", () => { host: "example.org", }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.getBaseUrl()).toBe("http://example.org"); }); @@ -166,7 +166,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify({ foo: "bar" }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.method).toBe("POST"); }); @@ -177,7 +177,7 @@ describe("WebApiAdapter", () => { host: "example.com", }), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); expect(adapter.method).toBe("GET"); }); }); @@ -193,7 +193,7 @@ describe("WebApiAdapter", () => { }), body: JSON.stringify(sampleJson), }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const response = await adapter.send({ bodyType: "json" as const, body: sampleJson, @@ -215,7 +215,7 @@ describe("WebApiAdapter", () => { }), body: "plain text", }); - const adapter = new WebApiAdapter(request); + const adapter = new WebApiAdapter(request, Response); const response = await adapter.send({ status: 200, body: "Some text", diff --git a/src/handlers/platforms/fetch-api/platform-adapter.ts b/src/handlers/platforms/fetch-api/platform-adapter.ts index 692dd82a..41a043db 100644 --- a/src/handlers/platforms/fetch-api/platform-adapter.ts +++ b/src/handlers/platforms/fetch-api/platform-adapter.ts @@ -18,8 +18,17 @@ export type WebApiHandler = (req: Request) => Response | Promise; * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request} * * */ -export class WebApiAdapter implements PlatformAdapterInterface { - constructor(public request: Request) {} +export class WebApiAdapter< + TRequest extends Request = Request, + TResponse extends Response = Response, +> implements PlatformAdapterInterface +{ + constructor( + public request: TRequest, + // todo how to type this nightmare + // maybe instead of constructor, pass instance and clone it? + public ResponseConstructor: { new (...args: any): TResponse }, + ) {} getHeader(name: string) { return this.request.headers.get(name); @@ -62,10 +71,10 @@ export class WebApiAdapter implements PlatformAdapterInterface { + async send(result: ActionHandlerResult): Promise { const body = result.bodyType === "json" ? JSON.stringify(result.body) : result.body; - return new Response(body, { + return new this.ResponseConstructor(body, { status: result.status, headers: { "Content-Type": result.bodyType === "json" ? "application/json" : "text/plain", diff --git a/src/handlers/platforms/fetch-api/saleor-webhooks/saleor-webhook.ts b/src/handlers/platforms/fetch-api/saleor-webhooks/saleor-webhook.ts index f1d9ac5a..ef9d1284 100644 --- a/src/handlers/platforms/fetch-api/saleor-webhooks/saleor-webhook.ts +++ b/src/handlers/platforms/fetch-api/saleor-webhooks/saleor-webhook.ts @@ -14,18 +14,19 @@ export type WebhookConfig GenericWebhookConfig; /** Function type provided by consumer in `SaleorWebApiWebhook.createHandler` */ -export type WebApiWebhookHandler = ( - req: Request, - ctx: WebhookContext & TExtras -) => Response | Promise; - -export abstract class SaleorWebApiWebhook< +export type WebApiWebhookHandler< TPayload = unknown, - TExtras extends Record = {} -> extends GenericSaleorWebhook { - createHandler(handlerFn: WebApiWebhookHandler): WebApiHandler { + TRequest extends Request = Request, + TResponse extends Response = Response, +> = (req: TRequest, ctx: WebhookContext) => TResponse | Promise; + +export abstract class SaleorWebApiWebhook extends GenericSaleorWebhook< + WebApiHandlerInput, + TPayload +> { + createHandler(handlerFn: WebApiWebhookHandler): WebApiHandler { return async (req) => { - const adapter = new WebApiAdapter(req); + const adapter = new WebApiAdapter(req, Response); const prepareRequestResult = await super.prepareRequest(adapter); if (prepareRequestResult.result === "sendResponse") { @@ -34,7 +35,6 @@ export abstract class SaleorWebApiWebhook< debug("Incoming request validated. Call handlerFn"); return handlerFn(req, { - ...(this.extraContext ?? ({} as TExtras)), ...prepareRequestResult.context, }); }; diff --git a/src/handlers/platforms/next-app-router/create-app-register-handler.ts b/src/handlers/platforms/next-app-router/create-app-register-handler.ts new file mode 100644 index 00000000..ef15388b --- /dev/null +++ b/src/handlers/platforms/next-app-router/create-app-register-handler.ts @@ -0,0 +1,21 @@ +import { RegisterActionHandler } from "@/handlers/actions/register-action-handler"; +import { GenericCreateAppRegisterHandlerOptions } from "@/handlers/shared/create-app-register-handler-types"; + +import { + NextAppRouterAdapter, + NextAppRouterHandler, + NextAppRouterHandlerInput, +} from "./platform-adapter"; + +export type CreateAppRegisterHandlerOptions = + GenericCreateAppRegisterHandlerOptions; + +export const createAppRegisterHandler = + (config: CreateAppRegisterHandlerOptions): NextAppRouterHandler => + async (req) => { + const adapter = new NextAppRouterAdapter(req); + const useCase = new RegisterActionHandler(adapter); + const result = await useCase.handleAction(config); + + return adapter.send(result); + }; diff --git a/src/handlers/platforms/next-app-router/create-manifest-handler.ts b/src/handlers/platforms/next-app-router/create-manifest-handler.ts new file mode 100644 index 00000000..c06bfdc8 --- /dev/null +++ b/src/handlers/platforms/next-app-router/create-manifest-handler.ts @@ -0,0 +1,40 @@ +import { NextRequest } from "next/server"; + +import { + CreateManifestHandlerOptions as GenericHandlerOptions, + ManifestActionHandler, +} from "@/handlers/actions/manifest-action-handler"; + +import { + NextAppRouterAdapter, + NextAppRouterHandler, + NextAppRouterHandlerInput, +} from "./platform-adapter"; + +export type CreateManifestHandlerOptions = GenericHandlerOptions; + +/** Returns app manifest API route handler for Web API compatible request handlers + * (examples: Next.js app router, hono, deno, etc.) + * that use signature: (req: Request) => Response + * where Request and Response are Fetch API objects + * + * App manifest is an endpoint that Saleor will call your App metadata. + * It has the App's name and description, as well as all the necessary information to + * register webhooks, permissions, and extensions. + * + * **Recommended path**: `/api/manifest` + * + * To learn more check Saleor docs + * @see {@link https://docs.saleor.io/developer/extending/apps/architecture/app-requirements#manifest-url} + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request} + * */ +export const createManifestHandler = + (config: CreateManifestHandlerOptions): NextAppRouterHandler => + async (request: NextRequest) => { + const adapter = new NextAppRouterAdapter(request); + const actionHandler = new ManifestActionHandler(adapter); + const result = await actionHandler.handleAction(config); + return adapter.send(result); + }; diff --git a/src/handlers/platforms/next-app-router/create-protected-handler.ts b/src/handlers/platforms/next-app-router/create-protected-handler.ts new file mode 100644 index 00000000..6d36dd04 --- /dev/null +++ b/src/handlers/platforms/next-app-router/create-protected-handler.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { APL } from "@/APL"; +import { + ProtectedActionValidator, + ProtectedHandlerContext, +} from "@/handlers/shared/protected-action-validator"; +import { Permission } from "@/types"; + +import { NextAppRouterAdapter, NextAppRouterHandler } from "./platform-adapter"; + +export type NextAppRouterProtectedHandler = ( + request: NextRequest, + ctx: ProtectedHandlerContext, +) => NextResponse | Promise; + +export const createProtectedHandler = + ( + handlerFn: NextAppRouterProtectedHandler, + apl: APL, + requiredPermissions?: Permission[], + ): NextAppRouterHandler => + async (request) => { + const adapter = new NextAppRouterAdapter(request); + const actionValidator = new ProtectedActionValidator(adapter); + const validationResult = await actionValidator.validateRequest({ + apl, + requiredPermissions, + }); + + if (validationResult.result === "failure") { + return adapter.send(validationResult.value); + } + + const context = validationResult.value; + try { + return await handlerFn(request, context); + } catch (err) { + return new NextResponse("Unexpected server error", { status: 500 }); + } + }; diff --git a/src/handlers/platforms/next-app-router/index.ts b/src/handlers/platforms/next-app-router/index.ts new file mode 100644 index 00000000..edd9e6e9 --- /dev/null +++ b/src/handlers/platforms/next-app-router/index.ts @@ -0,0 +1,7 @@ +export * from "./create-app-register-handler"; +export * from "./create-manifest-handler"; +export * from "./create-protected-handler"; +export * from "./platform-adapter"; +export * from "./saleor-webhooks/saleor-async-webhook"; +export * from "./saleor-webhooks/saleor-sync-webhook"; +export { NextAppRouterWebhookHandler } from "./saleor-webhooks/saleor-webhook"; diff --git a/src/handlers/platforms/next-app-router/platform-adapter.ts b/src/handlers/platforms/next-app-router/platform-adapter.ts new file mode 100644 index 00000000..7f94d61f --- /dev/null +++ b/src/handlers/platforms/next-app-router/platform-adapter.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { WebApiAdapter } from "@/handlers/platforms/fetch-api"; + +export type NextAppRouterHandlerInput = NextRequest; +export type NextAppRouterHandler = (req: NextRequest) => NextResponse | Promise; + +export class NextAppRouterAdapter extends WebApiAdapter { + constructor(public request: NextRequest) { + super(request, NextResponse); + } +} diff --git a/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-async-webhook.ts b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-async-webhook.ts new file mode 100644 index 00000000..b7c5eb43 --- /dev/null +++ b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-async-webhook.ts @@ -0,0 +1,24 @@ +import { AsyncWebhookEventType } from "@/types"; + +import { NextAppRouterHandler } from "../platform-adapter"; +import { + NextAppRouterWebhookHandler, + SaleorNextAppRouterWebhook, + WebhookConfig, +} from "./saleor-webhook"; + +export class SaleorAsyncWebhook extends SaleorNextAppRouterWebhook { + readonly event: AsyncWebhookEventType; + + protected readonly eventType = "async" as const; + + constructor(configuration: WebhookConfig) { + super(configuration); + + this.event = configuration.event; + } + + createHandler(handlerFn: NextAppRouterWebhookHandler): NextAppRouterHandler { + return super.createHandler(handlerFn); + } +} diff --git a/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-sync-webhook.ts b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-sync-webhook.ts new file mode 100644 index 00000000..c2b46f2f --- /dev/null +++ b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-sync-webhook.ts @@ -0,0 +1,29 @@ +import { SyncWebhookEventType } from "@/types"; + +import { NextAppRouterHandler } from "../platform-adapter"; +import { + NextAppRouterWebhookHandler, + SaleorNextAppRouterWebhook, + WebhookConfig, +} from "./saleor-webhook"; + +export type NextAppRouterSyncWebhookHandler = NextAppRouterWebhookHandler; + +export class SaleorSyncWebhook< + TPayload = unknown, + TEvent extends SyncWebhookEventType = SyncWebhookEventType, +> extends SaleorNextAppRouterWebhook { + readonly event: TEvent; + + protected readonly eventType = "sync" as const; + + constructor(configuration: WebhookConfig) { + super(configuration); + + this.event = configuration.event; + } + + createHandler(handlerFn: NextAppRouterSyncWebhookHandler): NextAppRouterHandler { + return super.createHandler(handlerFn); + } +} diff --git a/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-webhook.ts b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-webhook.ts new file mode 100644 index 00000000..568288ef --- /dev/null +++ b/src/handlers/platforms/next-app-router/saleor-webhooks/saleor-webhook.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { createDebug } from "@/debug"; +import { WebApiWebhookHandler } from "@/handlers/platforms/fetch-api"; +import { + GenericSaleorWebhook, + GenericWebhookConfig, +} from "@/handlers/shared/generic-saleor-webhook"; +import { AsyncWebhookEventType, SyncWebhookEventType } from "@/types"; + +import { + NextAppRouterAdapter, + NextAppRouterHandler, + NextAppRouterHandlerInput, +} from "../platform-adapter"; + +const debug = createDebug("SaleorWebhook"); + +export type WebhookConfig = + GenericWebhookConfig; + +export type NextAppRouterWebhookHandler< + TPayload = unknown, + TRequest extends NextRequest = NextRequest, + TResponse extends NextResponse = NextResponse, +> = WebApiWebhookHandler; + +export abstract class SaleorNextAppRouterWebhook extends GenericSaleorWebhook< + NextAppRouterHandlerInput, + TPayload +> { + createHandler(handlerFn: NextAppRouterWebhookHandler): NextAppRouterHandler { + return async (req): Promise => { + const adapter = new NextAppRouterAdapter(req); + const prepareRequestResult = await super.prepareRequest(adapter); + + if (prepareRequestResult.result === "sendResponse") { + return prepareRequestResult.response; + } + + debug("Incoming request validated. Call handlerFn"); + return handlerFn(req, { + ...prepareRequestResult.context, + }); + }; + } +} diff --git a/src/handlers/platforms/next/create-app-register-handler.test.ts b/src/handlers/platforms/next/create-app-register-handler.test.ts index 45d3f42b..5a5f5bf0 100644 --- a/src/handlers/platforms/next/create-app-register-handler.test.ts +++ b/src/handlers/platforms/next/create-app-register-handler.test.ts @@ -2,9 +2,9 @@ import { createMocks } from "node-mocks-http"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { APL, AuthData } from "@/APL"; -import { SALEOR_API_URL_HEADER } from "@/const"; -import * as fetchRemoteJwksModule from "@/fetch-remote-jwks"; +import * as fetchRemoteJwksModule from "@/auth/fetch-remote-jwks"; import * as getAppIdModule from "@/get-app-id"; +import { SALEOR_API_URL_HEADER } from "@/headers"; import { MockAPL } from "@/test-utils/mock-apl"; import { @@ -134,19 +134,19 @@ describe("Next.js createAppRegisterHandler", () => { expect.objectContaining({ authToken: "mock-auth-token", saleorApiUrl: "https://mock-saleor-domain.saleor.cloud/graphql/", - }) + }), ); expect(mockOnRequestVerified).toHaveBeenCalledWith( expect.anything(/* Assume original request */), expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplSaved).toHaveBeenCalledWith( expect.anything(/* Assume original request */), expect.objectContaining({ authData: expectedAuthData, - }) + }), ); expect(mockOnAuthAplFailed).not.toHaveBeenCalled(); }); @@ -198,7 +198,7 @@ describe("Next.js createAppRegisterHandler", () => { error: expect.objectContaining({ message: "test error", }), - }) + }), ); }); @@ -209,7 +209,7 @@ describe("Next.js createAppRegisterHandler", () => { context.respondWithError({ status: 401, message: "test message", - }) + }), ); const { res, req } = createMocks({ diff --git a/src/handlers/platforms/next/create-manifest-handler.test.ts b/src/handlers/platforms/next/create-manifest-handler.test.ts index e8928107..50b0c495 100644 --- a/src/handlers/platforms/next/create-manifest-handler.test.ts +++ b/src/handlers/platforms/next/create-manifest-handler.test.ts @@ -1,7 +1,7 @@ import { createMocks } from "node-mocks-http"; import { describe, expect, it, vi } from "vitest"; -import { SALEOR_SCHEMA_VERSION } from "@/const"; +import { SALEOR_SCHEMA_VERSION_HEADER } from "@/headers"; import { createManifestHandler, CreateManifestHandlerOptions } from "./create-manifest-handler"; @@ -13,7 +13,7 @@ describe("Next.js createManifestHandler", () => { headers: { host: "some-app-host.cloud", "x-forwarded-proto": "https", - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, method: "GET", }); diff --git a/src/handlers/platforms/next/saleor-webhooks/saleor-webhook.ts b/src/handlers/platforms/next/saleor-webhooks/saleor-webhook.ts index 075ec113..f8ce086a 100644 --- a/src/handlers/platforms/next/saleor-webhooks/saleor-webhook.ts +++ b/src/handlers/platforms/next/saleor-webhooks/saleor-webhook.ts @@ -15,21 +15,21 @@ const debug = createDebug("SaleorWebhook"); export type WebhookConfig = GenericWebhookConfig; -export type NextJsWebhookHandler = ( +export type NextJsWebhookHandler = ( req: NextApiRequest, res: NextApiResponse, - ctx: WebhookContext & TExtras + ctx: WebhookContext, ) => unknown | Promise; -export abstract class SaleorWebhook< - TPayload = unknown, - TExtras extends Record = {} -> extends GenericSaleorWebhook { +export abstract class SaleorWebhook extends GenericSaleorWebhook< + NextApiRequest, + TPayload +> { /** * Wraps provided function, to ensure incoming request comes from registered Saleor instance. * Also provides additional `context` object containing typed payload and request properties. */ - createHandler(handlerFn: NextJsWebhookHandler): NextApiHandler { + createHandler(handlerFn: NextJsWebhookHandler): NextApiHandler { return async (req, res) => { const adapter = new NextJsAdapter(req, res); const prepareRequestResult = await super.prepareRequest(adapter); @@ -40,7 +40,6 @@ export abstract class SaleorWebhook< debug("Incoming request validated. Call handlerFn"); return handlerFn(req, res, { - ...(this.extraContext ?? ({} as TExtras)), ...prepareRequestResult.context, }); }; diff --git a/src/handlers/shared/generic-saleor-webhook.ts b/src/handlers/shared/generic-saleor-webhook.ts index 66ab1360..f031cece 100644 --- a/src/handlers/shared/generic-saleor-webhook.ts +++ b/src/handlers/shared/generic-saleor-webhook.ts @@ -19,7 +19,7 @@ const debug = createDebug("SaleorWebhook"); export interface GenericWebhookConfig< RequestType, - Event = AsyncWebhookEventType | SyncWebhookEventType + Event = AsyncWebhookEventType | SyncWebhookEventType, > { name?: string; webhookPath: string; @@ -29,22 +29,16 @@ export interface GenericWebhookConfig< onError?(error: WebhookError | Error, request: RequestType): void; formatErrorResponse?( error: WebhookError | Error, - request: RequestType + request: RequestType, ): Promise; query: string | ASTNode; } -export abstract class GenericSaleorWebhook< - TRequestType, - TPayload = unknown, - TExtras extends Record = {} -> { +export abstract class GenericSaleorWebhook { private webhookValidator = new SaleorWebhookValidator(); protected abstract eventType: "async" | "sync"; - protected extraContext?: TExtras; - name: string; webhookPath: string; @@ -123,7 +117,7 @@ export abstract class GenericSaleorWebhook< } protected async prepareRequest>( - adapter: Adapter + adapter: Adapter, ): Promise< | { result: "callHandler"; context: WebhookContext } | { result: "sendResponse"; response: ReturnType } diff --git a/src/handlers/shared/protected-action-validator.test.ts b/src/handlers/shared/protected-action-validator.test.ts index 7b917c0b..8c289878 100644 --- a/src/handlers/shared/protected-action-validator.test.ts +++ b/src/handlers/shared/protected-action-validator.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@/const"; +import * as verifyJWTModule from "@/auth/verify-jwt"; +import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@/headers"; import { MockAdapter } from "@/test-utils/mock-adapter"; import { MockAPL } from "@/test-utils/mock-apl"; import * as extractUserModule from "@/util/extract-user-from-jwt"; -import * as verifyJWTModule from "@/verify-jwt"; import { ProtectedActionValidator } from "./protected-action-validator"; @@ -18,15 +18,14 @@ describe("ProtectedActionValidator", () => { baseUrl: "https://example.com", mockHeaders: { [SALEOR_API_URL_HEADER]: mockAPL.workingSaleorApiUrl, - [SALEOR_AUTHORIZATION_BEARER_HEADER]: mockAPL.mockToken - } + [SALEOR_AUTHORIZATION_BEARER_HEADER]: mockAPL.mockToken, + }, }); - vi.spyOn(verifyJWTModule, "verifyJWT").mockResolvedValue(undefined); vi.spyOn(extractUserModule, "extractUserFromJwt").mockReturnValue({ email: "user@domain.com", - userPermissions: [] + userPermissions: [], }); }); @@ -76,8 +75,8 @@ describe("ProtectedActionValidator", () => { baseUrl: "https://example.com", mockHeaders: { // SALEOR_API_URL_HEADER is missing - [SALEOR_AUTHORIZATION_BEARER_HEADER]: mockAPL.mockToken - } + [SALEOR_AUTHORIZATION_BEARER_HEADER]: mockAPL.mockToken, + }, }); const validator = new ProtectedActionValidator(adapter); @@ -102,7 +101,7 @@ describe("ProtectedActionValidator", () => { mockHeaders: { [SALEOR_API_URL_HEADER]: mockAPL.workingSaleorApiUrl, // SALEOR_AUTHORIZATION_BEARER_HEADER is missing - } + }, }); const validator = new ProtectedActionValidator(adapter); @@ -143,7 +142,9 @@ describe("ProtectedActionValidator", () => { it("should fail validation when JWT verification fails", async () => { const validator = new ProtectedActionValidator(mockAdapter); - vi.spyOn(verifyJWTModule, "verifyJWT").mockRejectedValue(new Error("JWT verification failed")); + vi.spyOn(verifyJWTModule, "verifyJWT").mockRejectedValue( + new Error("JWT verification failed"), + ); const result = await validator.validateRequest({ apl: mockAPL, diff --git a/src/handlers/shared/protected-action-validator.ts b/src/handlers/shared/protected-action-validator.ts index 856f0321..e63eac6e 100644 --- a/src/handlers/shared/protected-action-validator.ts +++ b/src/handlers/shared/protected-action-validator.ts @@ -1,11 +1,11 @@ import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { APL, AuthData } from "@/APL"; +import { verifyJWT } from "@/auth/verify-jwt"; import { createDebug } from "@/debug"; import { getOtelTracer } from "@/open-telemetry"; import { Permission } from "@/types"; import { extractUserFromJwt, TokenUserPayload } from "@/util/extract-user-from-jwt"; -import { verifyJWT } from "@/verify-jwt"; import { ActionHandlerResult, PlatformAdapterInterface } from "./generic-adapter-use-case-types"; import { SaleorRequestProcessor } from "./saleor-request-processor"; @@ -191,7 +191,7 @@ export class ProtectedActionValidator { }, }; } - } + }, ); } } diff --git a/src/handlers/shared/saleor-request-processor.test.ts b/src/handlers/shared/saleor-request-processor.test.ts index bdf433fe..f02c1000 100644 --- a/src/handlers/shared/saleor-request-processor.test.ts +++ b/src/handlers/shared/saleor-request-processor.test.ts @@ -4,9 +4,9 @@ import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_EVENT_HEADER, - SALEOR_SCHEMA_VERSION, + SALEOR_SCHEMA_VERSION_HEADER, SALEOR_SIGNATURE_HEADER, -} from "@/const"; +} from "@/headers"; import { MockAdapter } from "@/test-utils/mock-adapter"; import { SaleorRequestProcessor } from "./saleor-request-processor"; @@ -77,7 +77,7 @@ describe("SaleorRequestProcessor", () => { [SALEOR_SIGNATURE_HEADER]: "signature-value", [SALEOR_EVENT_HEADER]: "event-name", [SALEOR_API_URL_HEADER]: "https://api.saleor.io", - [SALEOR_SCHEMA_VERSION]: "3.20", + [SALEOR_SCHEMA_VERSION_HEADER]: "3.20", }, }); const middleware = new SaleorRequestProcessor(adapter); diff --git a/src/handlers/shared/saleor-request-processor.ts b/src/handlers/shared/saleor-request-processor.ts index 68df7c38..5fb1269a 100644 --- a/src/handlers/shared/saleor-request-processor.ts +++ b/src/handlers/shared/saleor-request-processor.ts @@ -2,9 +2,9 @@ import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_EVENT_HEADER, - SALEOR_SCHEMA_VERSION, + SALEOR_SCHEMA_VERSION_HEADER, SALEOR_SIGNATURE_HEADER, -} from "@/const"; +} from "@/headers"; import { HTTPMethod, PlatformAdapterInterface } from "./generic-adapter-use-case-types"; @@ -53,7 +53,7 @@ export class SaleorRequestProcessor { * Saleor version 3.20 != 3.2. * Semver must be compared as strings */ - schemaVersion: this.toStringOrUndefined(this.adapter.getHeader(SALEOR_SCHEMA_VERSION)), + schemaVersion: this.toStringOrUndefined(this.adapter.getHeader(SALEOR_SCHEMA_VERSION_HEADER)), }; } } diff --git a/src/handlers/shared/saleor-webhook-validator.test.ts b/src/handlers/shared/saleor-webhook-validator.test.ts index 45fa112a..2d67446a 100644 --- a/src/handlers/shared/saleor-webhook-validator.test.ts +++ b/src/handlers/shared/saleor-webhook-validator.test.ts @@ -1,21 +1,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { AuthData } from "@/APL"; -import * as fetchRemoteJwksModule from "@/fetch-remote-jwks"; +import * as fetchRemoteJwksModule from "@/auth/fetch-remote-jwks"; +import * as verifySignatureModule from "@/auth/verify-signature"; import { MockAdapter } from "@/test-utils/mock-adapter"; import { MockAPL } from "@/test-utils/mock-apl"; -import * as verifySignatureModule from "@/verify-signature"; import { SaleorRequestProcessor } from "./saleor-request-processor"; import { SaleorWebhookValidator } from "./saleor-webhook-validator"; -vi.spyOn(verifySignatureModule, "verifySignatureFromApiUrl").mockImplementation( - async (domain, signature) => { - if (signature !== "mocked_signature") { - throw new Error("Wrong signature"); - } - }, -); vi.spyOn(verifySignatureModule, "verifySignatureWithJwks").mockImplementation( async (domain, signature) => { if (signature !== "mocked_signature") { diff --git a/src/handlers/shared/saleor-webhook-validator.ts b/src/handlers/shared/saleor-webhook-validator.ts index 58fa722c..681fbea4 100644 --- a/src/handlers/shared/saleor-webhook-validator.ts +++ b/src/handlers/shared/saleor-webhook-validator.ts @@ -1,12 +1,12 @@ import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { APL } from "@/APL"; +import { fetchRemoteJwks } from "@/auth/fetch-remote-jwks"; +import { verifySignatureWithJwks } from "@/auth/verify-signature"; import { createDebug } from "@/debug"; -import { fetchRemoteJwks } from "@/fetch-remote-jwks"; import { getOtelTracer } from "@/open-telemetry"; import { SaleorSchemaVersion } from "@/types"; -import { parseSchemaVersion } from "@/util"; -import { verifySignatureWithJwks } from "@/verify-signature"; +import { parseSchemaVersion } from "@/util/schema-version"; import { PlatformAdapterInterface } from "./generic-adapter-use-case-types"; import { SaleorRequestProcessor } from "./saleor-request-processor"; diff --git a/src/headers.ts b/src/headers.ts index 5570fdde..4892d711 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -1,10 +1,11 @@ -import { - SALEOR_API_URL_HEADER, - SALEOR_AUTHORIZATION_BEARER_HEADER, - SALEOR_EVENT_HEADER, - SALEOR_SCHEMA_VERSION, - SALEOR_SIGNATURE_HEADER, -} from "./const"; +export const SALEOR_EVENT_HEADER = "saleor-event"; +export const SALEOR_SIGNATURE_HEADER = "saleor-signature"; +export const SALEOR_AUTHORIZATION_BEARER_HEADER = "authorization-bearer"; +export const SALEOR_API_URL_HEADER = "saleor-api-url"; +/** + * Available when Saleor executes "manifest" or "token exchange" requests. + */ +export const SALEOR_SCHEMA_VERSION_HEADER = "saleor-schema-version"; const toStringOrUndefined = (value: string | string[] | undefined) => value ? value.toString() : undefined; @@ -17,7 +18,7 @@ export const getSaleorHeaders = (headers: { [name: string]: string | string[] | signature: toStringOrUndefined(headers[SALEOR_SIGNATURE_HEADER]), event: toStringOrUndefined(headers[SALEOR_EVENT_HEADER]), saleorApiUrl: toStringOrUndefined(headers[SALEOR_API_URL_HEADER]), - schemaVersion: toStringOrUndefined(headers[SALEOR_SCHEMA_VERSION]), + schemaVersion: toStringOrUndefined(headers[SALEOR_SCHEMA_VERSION_HEADER]), }); /** diff --git a/src/types.ts b/src/types.ts index 3da6923a..9a6de4f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -322,3 +322,5 @@ export interface AppManifest { } export type SaleorSchemaVersion = [major: number, minor: number]; + +export { LocaleCode } from "./locales"; diff --git a/src/urls.test.ts b/src/urls.test.ts deleted file mode 100644 index 5ecd6907..00000000 --- a/src/urls.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, test } from "vitest"; - -import { getJwksUrlFromSaleorApiUrl } from "./urls"; - -describe("urls.ts", () => { - describe("getJwksUrlFromSaleorApiUrl function", () => { - test("Resolves valid url from saleor api url", () => { - expect(getJwksUrlFromSaleorApiUrl("https://my-saleor.saleor.cloud")).toBe( - "https://my-saleor.saleor.cloud/.well-known/jwks.json" - ); - }); - }); -}); diff --git a/src/urls.ts b/src/urls.ts deleted file mode 100644 index 9006b4dd..00000000 --- a/src/urls.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const getJwksUrlFromSaleorApiUrl = (saleorApiUrl: string): string => - `${new URL(saleorApiUrl).origin}/.well-known/jwks.json`; diff --git a/src/util/index.ts b/src/util/index.ts deleted file mode 100644 index fe096549..00000000 --- a/src/util/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./is-in-iframe"; -export * from "./schema-version"; -export * from "./use-is-mounted"; diff --git a/src/util/public/browser/index.ts b/src/util/public/browser/index.ts new file mode 100644 index 00000000..d972f229 --- /dev/null +++ b/src/util/public/browser/index.ts @@ -0,0 +1,2 @@ +export * from "../../is-in-iframe"; +export * from "../../use-is-mounted"; diff --git a/tsup.config.ts b/tsup.config.ts index fe5b31fb..d24c612f 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,14 +2,12 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: { - const: "src/const.ts", types: "src/types.ts", - urls: "src/urls.ts", headers: "src/headers.ts", - util: "src/util/public/index.ts", + "util/index": "src/util/public/index.ts", + "util/browser": "src/util/public/browser/index.ts", "saleor-app": "src/saleor-app.ts", - "verify-jwt": "src/verify-jwt.ts", - "verify-signature": "src/verify-signature.ts", + "auth/index": "src/auth/index.ts", /** * APLs */ @@ -30,9 +28,7 @@ export default defineConfig({ "handlers/next/index": "src/handlers/platforms/next/index.ts", "handlers/fetch-api/index": "src/handlers/platforms/fetch-api/index.ts", "handlers/aws-lambda/index": "src/handlers/platforms/aws-lambda/index.ts", - - // Virtual export - "handlers/next-app-router/index": "src/handlers/platforms/fetch-api/index.ts", + "handlers/next-app-router/index": "src/handlers/platforms/next-app-router/index.ts", }, dts: true, clean: true,