diff --git a/docs/api-handlers.md b/docs/api-handlers.md index c26c4f5..3cb6f9c 100644 --- a/docs/api-handlers.md +++ b/docs/api-handlers.md @@ -57,10 +57,13 @@ Example usage of app register handler in Next.js // pages/api/register.ts import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; -import { VercelAPL } from "./vercel-apl"; +import { UpstashAPL } from "@saleor/app-sdk/APL"; export default createAppRegisterHandler({ - apl: new VercelAPL(), + apl: new UpstashAPL({ + restURL: "...", + restToken: "...", + }), allowedSaleorUrls: ["https://your-saleor.saleor.cloud/graphql/"], // optional, see options below }); ``` diff --git a/docs/apl.md b/docs/apl.md index 13e5042..f632526 100644 --- a/docs/apl.md +++ b/docs/apl.md @@ -124,12 +124,12 @@ To handle both scenarios, initialize the proper APLs in your code based on it's ```ts // lib/saleorApp.ts -import { FileAPL, VercelAPL } from "@saleor/app-sdk/APL"; +import { FileAPL, UpstashAPL } from "@saleor/app-sdk/APL"; // Based on environment variable the app will use a different APL: // - For local development store auth data in the `.auth-data.json`. -// - For app deployment on Vercel with Saleor CLI, use vercelAPL. -export const apl = process.env.VERCEL === "1" ? new VercelAPL() : new FileAPL(); +// - For app deployment on hosted environment like Vercel, use UpstashAPL +export const apl = process.env.APL === "upstash" ? new UpstashAPL() : new FileAPL(); ``` Now you can use it for in your view: @@ -166,26 +166,6 @@ File based storage of auth data, intended for local development. Data are stored Please note: this APL supports single tenant only (new registrations overwrite previous ones) and should not be used on production. -### VercelAPL - -Single tenant APL dedicated for apps deployed on Vercel. To use this APL you'll need to deploy application from the Marketplace or use the [Saleor CLI](https://docs.saleor.io/docs/3.x/cli). This way the required `SALEOR_REGISTER_APP_URL` and `SALEOR_DEPLOYMENT_TOKEN` variables will be set up automatically during the first deployment. During the registration process Saleor's service will set up auth data in the environment variables and trigger the deployment: - -```mermaid -sequenceDiagram - participant SI as Saleor Instance - participant A as App - participant SSI as Saleor x Vercel integration - participant V as Vercel - - SI->>+A: Register - A->>SSI: Update auth data - A->>-SI: Register completed - SSI->>V: Set auth data as environment variables - V->>A: Redeploy the application -``` - -If auth data are already saved in the environment, registration will proceed only if the domain of new request is the same as the previous one. This check is made to allow reinstalling the application possible and prevent unintended 3rd party to overwrite existing data. If you want to change the domain of registered Saleor domain, remove `SALEOR_DOMAIN` and `SALEOR_AUTH_TOKEN` environment variables from your Vercel project and redeploy it to refresh it's context. - ### UpstashAPL [Upstash](https://upstash.com) is a Redis SaaS targeting serverless applications. It's free tier is more than enough to start developing multi-tenant Saleor Apps, and credit card info is not required to create an account. diff --git a/src/APL/index.ts b/src/APL/index.ts index 4ce2467..5a036ee 100644 --- a/src/APL/index.ts +++ b/src/APL/index.ts @@ -2,4 +2,3 @@ export * from "./apl"; export * from "./file-apl"; export * from "./saleor-cloud-apl"; export * from "./upstash-apl"; -export * from "./vercel-apl"; diff --git a/src/APL/vercel-apl.test.ts b/src/APL/vercel-apl.test.ts deleted file mode 100644 index c35ce45..0000000 --- a/src/APL/vercel-apl.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { AuthData } from "./apl"; -import { VercelAPL, VercelAPLVariables } from "./vercel-apl"; - -const fetchMock = vi.fn(); - -vi.stubGlobal("fetch", fetchMock); - -// now you can access it as `IntersectionObserver` or `window.IntersectionObserver` -const aplConfig = { - deploymentToken: "token", - registerAppURL: "http://example.com", -}; - -const stubAuthData: AuthData = { - domain: "example.com", - token: "example-token", - saleorApiUrl: "https://example.com/graphql/", - appId: "42", - jwks: "{}", -}; - -describe("APL", () => { - const initialEnv = { ...process.env }; - - afterEach(() => { - process.env = { ...initialEnv }; - vi.resetModules(); - }); - - describe("VercelAPL", () => { - it("Constructs VercelAPL instance when deploymentToken and registerAppURL provided", async () => { - expect(() => new VercelAPL(aplConfig)).not.toThrow(); - }); - - it("Constructs VercelAPL instance with config values from environment variables", async () => { - process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = aplConfig.registerAppURL; - process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = aplConfig.deploymentToken; - - expect(() => new VercelAPL()).not.toThrow(); - }); - - it("Test if constructor use options over environment variables", async () => { - process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = "environment"; - process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = "environment"; - - const apl = await new VercelAPL({ deploymentToken: "option", registerAppURL: "option" }); - // eslint-disable-next-line dot-notation - expect(apl["deploymentToken"]).toBe("option"); - // eslint-disable-next-line dot-notation - expect(apl["registerAppURL"]).toBe("option"); - }); - - describe("set", () => { - it("Successful save of the auth data", async () => { - // @ts-ignore Ignore type of mocked response - fetchMock.mockResolvedValue({ status: 200 }); - const apl = new VercelAPL({ - registerAppURL: "https://registerService.example.com", - deploymentToken: "token", - }); - await apl.set(stubAuthData); - expect(fetchMock).toBeCalledWith( - "https://registerService.example.com", - - { - body: JSON.stringify({ - token: "token", - envs: [ - { - key: VercelAPLVariables.AUTH_DATA_VARIABLE_NAME, - value: JSON.stringify(stubAuthData), - }, - ], - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - } - ); - }); - - it("Successful save of the auth data during reinstallation for the same domain", async () => { - process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME] = JSON.stringify(stubAuthData); - - // @ts-ignore Ignore type of mocked response - fetchMock.mockResolvedValue({ status: 200 }); - const apl = new VercelAPL({ - registerAppURL: "https://registerService.example.com", - deploymentToken: "token", - }); - await apl.set({ ...stubAuthData, token: "new_token" }); - expect(fetchMock).toBeCalledWith( - "https://registerService.example.com", - - { - body: JSON.stringify({ - token: "token", - envs: [ - { - key: VercelAPLVariables.AUTH_DATA_VARIABLE_NAME, - value: JSON.stringify({ ...stubAuthData, token: "new_token" }), - }, - ], - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - } - ); - }); - - it("Reject save of the auth data during reinstallation for a different domain", async () => { - process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME] = JSON.stringify(stubAuthData); - - // @ts-ignore Ignore type of mocked response - fetchMock.mockResolvedValue({ status: 200 }); - const apl = new VercelAPL({ - registerAppURL: "https://registerService.example.com", - deploymentToken: "token", - }); - await expect( - apl.set({ ...stubAuthData, domain: "different.domain.example.com" }) - ).rejects.toThrow( - "Vercel APL was not able to save auth data, application already registered" - ); - }); - - it("Raise error when register service returns non 200 response", async () => { - // @ts-ignore Ignore type of mocked response - fetchMock.mockResolvedValue({ status: 500 }); - - const apl = new VercelAPL({ - registerAppURL: "https://registerService.example.com/internalError", - deploymentToken: "token", - }); - await expect(apl.set(stubAuthData)).rejects.toThrow( - "Vercel APL was not able to save auth data, register service responded with the code 500" - ); - }); - }); - - describe("get", () => { - describe("Read existing auth data from env", () => { - it("Read existing auth data", async () => { - process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME] = JSON.stringify(stubAuthData); - - const apl = new VercelAPL(aplConfig); - - expect(await apl.get(stubAuthData.saleorApiUrl)).toStrictEqual(stubAuthData); - }); - - it("Return undefined when unknown api url requested", async () => { - process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME] = JSON.stringify(stubAuthData); - - const apl = new VercelAPL(aplConfig); - - expect(await apl.get("https://unknown-domain.example.com/graphql/")).toBe(undefined); - }); - - it("Return undefined when no data is defined", async () => { - delete process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME]; - - const apl = new VercelAPL(aplConfig); - - expect(await apl.get("https://example.com/graphql/")).toBe(undefined); - }); - }); - }); - - describe("getAll", () => { - describe("Read existing auth data from env", () => { - it("Read existing auth data", async () => { - process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME] = JSON.stringify(stubAuthData); - - const apl = new VercelAPL(aplConfig); - - expect(await apl.getAll()).toStrictEqual([stubAuthData]); - }); - - it("Return empty list when no auth data are existing", async () => { - delete process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME]; - - const apl = new VercelAPL(aplConfig); - - expect(await apl.getAll()).toStrictEqual([]); - }); - }); - }); - - describe("isReady", () => { - it("Returns error with message mentioning missing env variables", async () => { - const apl = new VercelAPL(aplConfig); - - const result = await apl.isReady(); - - if (!result.ready) { - expect(result.error.message).toEqual( - "Env variables: \"SALEOR_AUTH_DATA\", \"SALEOR_REGISTER_APP_URL\", \"SALEOR_DEPLOYMENT_TOKEN\" not found or is empty. Ensure env variables exist" - ); - } else { - throw new Error("This should not happen"); - } - }); - }); - }); -}); diff --git a/src/APL/vercel-apl.ts b/src/APL/vercel-apl.ts deleted file mode 100644 index 6f31809..0000000 --- a/src/APL/vercel-apl.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* eslint-disable class-methods-use-this */ -// eslint-disable-next-line max-classes-per-file -import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "./apl"; -import { createAPLDebug } from "./apl-debug"; - -const debug = createAPLDebug("VercelAPL"); - -export const VercelAPLVariables = { - AUTH_DATA_VARIABLE_NAME: "SALEOR_AUTH_DATA", - SALEOR_REGISTER_APP_URL: "SALEOR_REGISTER_APP_URL", - SALEOR_DEPLOYMENT_TOKEN: "SALEOR_DEPLOYMENT_TOKEN", -}; - -export class VercelAplNotReadyError extends Error { - constructor(public missingEnvVars: string[]) { - super( - `Env variables: ${missingEnvVars - .map((v) => `"${v}"`) - .join(", ")} not found or is empty. Ensure env variables exist` - ); - } -} - -export class VercelAplNotConfiguredError extends Error {} - -const getEnvAuth = (): AuthData | undefined => { - const authDataSerialized = process.env[VercelAPLVariables.AUTH_DATA_VARIABLE_NAME]; - if (!authDataSerialized) { - return undefined; - } - return JSON.parse(authDataSerialized) as AuthData; -}; - -export type VercelAPLConfig = { - registerAppURL?: string; - deploymentToken?: string; -}; -/** Vercel APL - * - * Use environment variables for auth data storage. To update data on existing deployment, - * there's Saleor microservice which update new values with the Vercel API and restarts the instance. - * - * This APL should be used for single tenant purposes due to its limitations: - * - only stores single auth data entry (setting up a new one will overwrite previous values) - * - changing the environment variables require server restart - * - * To avoid override of existing auth data, setting a new auth token is only allowed for the same domain. - * If you want to change registration to another domain, you have to remove `SALEOR_AUTH_TOKEN` and - * `SALEOR_DOMAIN` environment variables in [Vercel dashboard](https://vercel.com/docs/concepts/projects/environment-variables). - * - * With this APL we recommend deployment using the [Saleor CLI](https://docs.saleor.io/docs/3.x/cli), - * which automatically set up the required environment variables during deployment: - * - SALEOR_REGISTER_APP_URL: the URL for microservice which set up variables using Vercel API - * - SALEOR_DEPLOYMENT_TOKEN: token for your particular Vercel deployment - */ -export class VercelAPL implements APL { - private registerAppURL?: string; - - private deploymentToken?: string; - - constructor(config?: VercelAPLConfig) { - const registerAppURL = - config?.registerAppURL || process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL]; - - const deploymentToken = - config?.deploymentToken || process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN]; - - this.registerAppURL = registerAppURL; - this.deploymentToken = deploymentToken; - } - - private async saveDataToVercel(authData?: AuthData) { - if (!this.registerAppURL) { - throw new Error( - "VercelAPL is not configured. See https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md" - ); - } - - debug("saveDataToVercel() called with: %j", { - domain: authData?.domain, - token: authData?.token.substring(0, 4), - }); - let response: Response; - try { - response = await fetch(this.registerAppURL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - token: this.deploymentToken, - envs: [ - { - key: VercelAPLVariables.AUTH_DATA_VARIABLE_NAME, - value: authData ? JSON.stringify(authData) : "", - }, - ], - }), - }); - } catch (error) { - debug("Error during saving the data:", error); - throw new Error(`VercelAPL was not able to save auth data ${error}`); - } - if (response.status >= 400 || response.status < 200) { - debug("Non 200 response code. Register service responded with %j", response); - throw new Error( - `Vercel APL was not able to save auth data, register service responded with the code ${response.status}` - ); - } - debug("Register service responded successfully"); - } - - async get(saleorApiUrl: string) { - const authData = getEnvAuth(); - - if (authData && saleorApiUrl === authData.saleorApiUrl) { - return authData; - } - return undefined; - } - - async set(authData: AuthData) { - const existingAuthData = getEnvAuth(); - if (existingAuthData && existingAuthData.domain !== authData.domain) { - // Registering again should be available only for the already installed domain - throw new Error("Vercel APL was not able to save auth data, application already registered"); - } - await this.saveDataToVercel(authData); - } - - async delete(saleorApiUrl: string) { - if (saleorApiUrl === getEnvAuth()?.saleorApiUrl) { - // Override existing data with the empty values - await this.saveDataToVercel(); - } - } - - async getAll() { - const authData = getEnvAuth(); - if (!authData) { - return []; - } - return [authData]; - } - - // eslint-disable-next-line class-methods-use-this - async isReady(): Promise { - const invalidEnvKeys = Object.values(VercelAPLVariables).filter((key) => { - const envValue = process.env[key]; - - return !envValue || envValue.length === 0; - }); - - if (invalidEnvKeys.length > 0) { - return { - ready: false, - error: new VercelAplNotReadyError(invalidEnvKeys), - }; - } - - return { - ready: true, - }; - } - - async isConfigured(): Promise { - return this.registerAppURL && this.deploymentToken - ? { - configured: true, - } - : { - configured: false, - error: new VercelAplNotConfiguredError( - "VercelAPL not configured. Check if register URL and deployment token provided in constructor or env " - ), - }; - } -}