diff --git a/.eslintrc b/.eslintrc index aa6d44a..20e535a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,7 +9,7 @@ "plugin:@typescript-eslint/recommended", "prettier" // prettier *has* to be the last one, to avoid conflicting rules ], - "ignorePatterns": ["pnpm-lock.yaml"], + "ignorePatterns": ["pnpm-lock.yaml", "dist"], "plugins": ["simple-import-sort", "@typescript-eslint"], "rules": { "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], diff --git a/.prettierignore b/.prettierignore index d605ec8..c50265d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ saleor/api.tsx pnpm-lock.yaml graphql.schema.json lib/$path.ts +dist \ No newline at end of file diff --git a/docs/apl.md b/docs/apl.md index f6acb68..1a09968 100644 --- a/docs/apl.md +++ b/docs/apl.md @@ -14,7 +14,7 @@ APL is an interface for managing auth data of registered Apps. Implementing it d ## Example implementation -Let's create an APL, which uses redis for data storage: +Let's create an APL, which uses Redis for data storage: ```ts import { createClient } from "redis"; @@ -66,3 +66,74 @@ const handler = async (request) => { // the middleware will reject request if it's domain has not been registered export default withRegisteredSaleorDomainHeader({ apl: redisAPL })(handler); ``` + +### Using different APL depending on the environment + +Depending on the environment your app is working on, you may want to use a different APL. For example during local development you might like to use `FileAPL`, because it does not require any additional infrastructure. Deployed apps on the other hand need more robust solution. + +To handle both scenarios, initialize the proper APLs in your code based on it's environment. In your application code: + +```ts +// lib/saleorApp.ts + +import { FileAPL, VercelAPL } 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(); +``` + +Now you can use it for in your view: + +```ts +import { SALEOR_DOMAIN_HEADER } from "@saleor/app-sdk/const"; +import { withRegisteredSaleorDomainHeader } from "@saleor/app-sdk/middleware"; +import type { Handler } from "retes"; +import { toNextHandler } from "retes/adapter"; +import { Response } from "retes/response"; + +// import created APL +import { apl } from "@lib/saleorApp"; + +const handler: Handler = async (request) => { + const saleorDomain = request.headers[SALEOR_DOMAIN_HEADER]; + + // Get auth data + const authData = apl.get(saleorDomain); + + // view logic... + + return Response.OK(); +}; + +export default toNextHandler([withRegisteredSaleorDomainHeader({ apl }), handler]); +``` + +## Available APLs + +### FileAPL + +File based storage of auth data, intended for local development. Data are stored in the `.saleor-app-auth.json` file. You'll be able to develop app without additional dependencies or infrastructure. + +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. Apps deployed from Marketplace and CLI automatically set up Vercel project for this APL (`SALEOR_REGISTER_APP_URL` and `SALEOR_DEPLOYMENT_TOKEN` variables). + +The auth data are stored using environment variables: + +```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 +``` diff --git a/docs/debugging.md b/docs/debugging.md index cd8c5f8..3df6273 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -50,3 +50,4 @@ Use the namespace name to enable debug logs for each module. | \app-sdk:\* | Enable all | | app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) | | app-sdk:Middleware:\* | Enable all middlewares (node only) | +| app-sdk:APL:\* | Enable all APLs (node only) | diff --git a/package.json b/package.json index a16d6fc..dc72a86 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,13 @@ "fast-glob": "^3.2.11", "graphql": "^16.5.0", "jose": "^4.8.3", + "node-fetch": "^3.2.10", "retes": "^0.32.0", "uuid": "^8.3.2" }, "devDependencies": { "release-it": "^15.4.1", + "@types/node-fetch": "^2.6.2", "@testing-library/dom": "^8.17.1", "@types/debug": "^4.1.7", "@types/node": "^18.6.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c905d8..4f8666b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@testing-library/dom': ^8.17.1 '@types/debug': ^4.1.7 '@types/node': ^18.6.5 + '@types/node-fetch': ^2.6.2 '@types/uuid': ^8.3.4 '@typescript-eslint/eslint-plugin': ^5.33.0 '@typescript-eslint/parser': ^5.33.0 @@ -24,6 +25,7 @@ specifiers: husky: ^8.0.1 jose: ^4.8.3 jsdom: ^20.0.0 + node-fetch: ^3.2.10 prettier: 2.7.1 release-it: ^15.4.1 retes: ^0.32.0 @@ -40,6 +42,7 @@ dependencies: fast-glob: 3.2.11 graphql: 16.5.0 jose: 4.8.3 + node-fetch: 3.2.10 retes: 0.32.0 uuid: 8.3.2 @@ -47,6 +50,7 @@ devDependencies: '@testing-library/dom': 8.17.1 '@types/debug': 4.1.7 '@types/node': 18.7.1 + '@types/node-fetch': 2.6.2 '@types/uuid': 8.3.4 '@typescript-eslint/eslint-plugin': 5.33.0_njno5y7ry2l2lcmiu4tywxkwnq '@typescript-eslint/parser': 5.33.0_qugx7qdu5zevzvxaiqyxfiwquq @@ -686,6 +690,13 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/node-fetch/2.6.2: + resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} + dependencies: + '@types/node': 18.7.1 + form-data: 3.0.1 + dev: true + /@types/node/18.7.1: resolution: {integrity: sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==} dev: true @@ -1446,7 +1457,6 @@ packages: /data-uri-to-buffer/4.0.0: resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} engines: {node: '>= 12'} - dev: true /data-urls/3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} @@ -2358,7 +2368,6 @@ packages: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.2.1 - dev: true /figures/4.0.1: resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==} @@ -2418,6 +2427,15 @@ packages: engines: {node: '>= 14.17'} dev: true + /form-data/3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -2432,7 +2450,6 @@ packages: engines: {node: '>=12.20.0'} dependencies: fetch-blob: 3.2.0 - dev: true /fs-extra/8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} @@ -3494,7 +3511,6 @@ packages: /node-domexception/1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - dev: true /node-fetch/2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} @@ -3515,7 +3531,6 @@ packages: data-uri-to-buffer: 4.0.0 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - dev: true /node-releases/2.0.6: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} @@ -4899,7 +4914,6 @@ packages: /web-streams-polyfill/3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} - dev: true /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} diff --git a/src/APL/apl-debug.ts b/src/APL/apl-debug.ts new file mode 100644 index 0000000..e25b627 --- /dev/null +++ b/src/APL/apl-debug.ts @@ -0,0 +1,3 @@ +import { createDebug } from "../debug"; + +export const createAPLDebug = (namespace: string) => createDebug(`APL:${namespace}`); diff --git a/src/APL/file-apl.test.ts b/src/APL/file-apl.test.ts new file mode 100644 index 0000000..70eee80 --- /dev/null +++ b/src/APL/file-apl.test.ts @@ -0,0 +1,119 @@ +import { promises as fsPromises } from "fs"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { FileAPL } from "./file-apl"; + +const stubAuthData = { + domain: "example.com", + token: "example-token", +}; + +describe("APL", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("FileAPL", () => { + describe("get", () => { + it("Should throw error when JSON parse fails", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue("Not a valid JSON"); + + const apl = new FileAPL(); + await expect(apl.get(stubAuthData.domain)).rejects.toThrow( + "File APL could not read auth data from the .saleor-app-auth.json file" + ); + }); + + it("Returns auth data for existing domain", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData)); + + const apl = new FileAPL(); + + expect(await apl.get(stubAuthData.domain)).toStrictEqual(stubAuthData); + }); + + it("Returns undefined for unknown domain", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData)); + + const apl = new FileAPL(); + + expect(await apl.get("unknown-domain.example.com")).toBe(undefined); + }); + }); + + describe("set", () => { + it("Handle write file errors", async () => { + const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockImplementation(() => { + throw Error("Write error"); + }); + + const apl = new FileAPL(); + + await expect(apl.set(stubAuthData)).rejects.toThrow( + "File APL was unable to save auth data" + ); + expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", JSON.stringify(stubAuthData)); + }); + }); + + it("Successfully save to file", async () => { + const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue(); + + const apl = new FileAPL(); + + await expect(apl.set(stubAuthData)); + + expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", JSON.stringify(stubAuthData)); + }); + + describe("delete", () => { + it("Should override file when called with known domain", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData)); + const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue(); + + const apl = new FileAPL(); + + await apl.delete(stubAuthData.domain); + + expect(spyWriteFile).toBeCalledWith(".saleor-app-auth.json", "{}"); + }); + + it("Should not delete data when called with unknown domain", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData)); + + const spyWriteFile = vi.spyOn(fsPromises, "writeFile").mockResolvedValue(); + + const apl = new FileAPL(); + + await apl.delete("unknown-domain.example.com"); + + expect(spyWriteFile).toBeCalledTimes(0); + }); + }); + + describe("getAll", () => { + it("Should return list with one item when auth data are existing", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue(JSON.stringify(stubAuthData)); + + const apl = new FileAPL(); + + expect(await apl.getAll()).toStrictEqual([stubAuthData]); + }); + + it("Should return empty list when auth data are empty", async () => { + vi.spyOn(fsPromises, "access").mockResolvedValue(); + vi.spyOn(fsPromises, "readFile").mockResolvedValue("{}"); + + const apl = new FileAPL(); + + expect(await apl.getAll()).toStrictEqual([]); + }); + }); + }); +}); diff --git a/src/APL/file-apl.ts b/src/APL/file-apl.ts new file mode 100644 index 0000000..4f52b9e --- /dev/null +++ b/src/APL/file-apl.ts @@ -0,0 +1,101 @@ +import { promises as fsPromises } from "fs"; + +import { APL, AuthData } from "./apl"; +import { createAPLDebug } from "./apl-debug"; + +const debug = createAPLDebug("FileAPL"); + +export type FileAPLConfig = { + fileName?: string; +}; + +/** + * File APL + * + * The APL store auth data in the json file. + * + * Before using this APL, please take in consideration: + * - only stores single auth data entry (setting up a new one will overwrite previous values) + * - it's not recommended for production use - redeployment of the application will override + * existing values, or data persistence will not be guaranteed at all depending on chosen + * hosting solution + * + */ +export class FileAPL implements APL { + private fileName: string; + + constructor(config: FileAPLConfig = {}) { + this.fileName = config?.fileName || ".saleor-app-auth.json"; + } + + /** + * Load auth data from a file and return it as AuthData format. + * In case of incomplete or invalid data, return `undefined`. + * + * @param {string} fileName + */ + private async loadDataFromFile(): Promise { + debug(`Load auth data from the ${this.fileName} file`); + let parsedData: Record = {}; + try { + await fsPromises.access(this.fileName); + parsedData = JSON.parse(await fsPromises.readFile(this.fileName, "utf-8")); + } catch (err) { + debug(`Could not read auth data from the ${this.fileName} file`, err); + throw new Error(`File APL could not read auth data from the ${this.fileName} file`); + } + const { token, domain } = parsedData; + if (token && domain) { + return { token, domain }; + } + return undefined; + } + + /** + * Save auth data to file. + * When `authData` argument is empty, will overwrite file with empty values. + * + * @param {string} fileName + * @param {AuthData} [authData] + */ + private async saveDataToFile(authData?: AuthData) { + debug(`Save auth data to the ${this.fileName} file`); + const newData = authData ? JSON.stringify(authData) : "{}"; + try { + await fsPromises.writeFile(this.fileName, newData); + } catch (err) { + debug(`Could not save auth data to the ${this.fileName} file`, err); + throw new Error("File APL was unable to save auth data"); + } + } + + async get(domain: string) { + const authData = await this.loadDataFromFile(); + if (domain === authData?.domain) { + return authData; + } + return undefined; + } + + async set(authData: AuthData) { + await this.saveDataToFile(authData); + } + + async delete(domain: string) { + const authData = await this.loadDataFromFile(); + + if (domain === authData?.domain) { + await this.saveDataToFile(); + } + } + + async getAll() { + const authData = await this.loadDataFromFile(); + + if (!authData) { + return []; + } + + return [authData]; + } +} diff --git a/src/APL/index.ts b/src/APL/index.ts index 3831bbc..0598c52 100644 --- a/src/APL/index.ts +++ b/src/APL/index.ts @@ -1 +1,3 @@ export * from "./apl"; +export * from "./file-apl"; +export * from "./vercel-apl"; diff --git a/src/APL/vercel-apl.test.ts b/src/APL/vercel-apl.test.ts new file mode 100644 index 0000000..1938898 --- /dev/null +++ b/src/APL/vercel-apl.test.ts @@ -0,0 +1,112 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { VercelAPL, VercelAPLVariables } from "./vercel-apl"; + +const aplConfig = { + deploymentToken: "token", + registerAppURL: "http://example.com", +}; + +const stubAuthData = { + domain: "example.com", + token: "example-token", +}; + +describe("APL", () => { + const initialEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...initialEnv }; + }); + + describe("VercelAPL", () => { + describe("constructor", () => { + it("Raise an error when configuration is missing", async () => { + delete process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL]; + process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN] = "token"; + + expect(() => new VercelAPL()).toThrow(); + + process.env[VercelAPLVariables.SALEOR_REGISTER_APP_URL] = "http://example.com"; + delete process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN]; + + expect(() => new VercelAPL()).toThrow(); + }); + }); + + 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("get", () => { + describe("Read existing auth data from env", () => { + it("Read existing auth data", async () => { + process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token; + process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain; + + const apl = new VercelAPL(aplConfig); + + expect(await apl.get(stubAuthData.domain)).toStrictEqual(stubAuthData); + }); + + it("Return undefined when unknown domain requested", async () => { + process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token; + process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain; + + const apl = new VercelAPL(aplConfig); + + expect(await apl.get("unknown-domain.example.com")).toBe(undefined); + }); + + it("Return undefined when no data is defined", async () => { + delete process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME]; + delete process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME]; + + const apl = new VercelAPL(aplConfig); + + expect(await apl.get("example.com")).toBe(undefined); + }); + }); + }); + + describe("getAll", () => { + describe("Read existing auth data from env", () => { + it("Read existing auth data", async () => { + process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME] = stubAuthData.token; + process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME] = stubAuthData.domain; + + 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.TOKEN_VARIABLE_NAME]; + delete process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME]; + + const apl = new VercelAPL(aplConfig); + + expect(await apl.getAll()).toStrictEqual([]); + }); + }); + }); + }); +}); diff --git a/src/APL/vercel-apl.ts b/src/APL/vercel-apl.ts new file mode 100644 index 0000000..a529e25 --- /dev/null +++ b/src/APL/vercel-apl.ts @@ -0,0 +1,114 @@ +/* eslint-disable class-methods-use-this */ +import fetch from "node-fetch"; + +import { APL, AuthData } from "./apl"; +import { createAPLDebug } from "./apl-debug"; + +const debug = createAPLDebug("VercelAPL"); + +export const VercelAPLVariables = { + TOKEN_VARIABLE_NAME: "SALEOR_AUTH_TOKEN", + DOMAIN_VARIABLE_NAME: "SALEOR_DOMAIN", + SALEOR_REGISTER_APP_URL: "SALEOR_REGISTER_APP_URL", + SALEOR_DEPLOYMENT_TOKEN: "SALEOR_DEPLOYMENT_TOKEN", +}; + +const getEnvAuth = (): AuthData | undefined => { + const token = process.env[VercelAPLVariables.TOKEN_VARIABLE_NAME]; + const domain = process.env[VercelAPLVariables.DOMAIN_VARIABLE_NAME]; + if (!token || !domain) { + return undefined; + } + return { + token, + domain, + }; +}; + +export type VercelAPLConfig = { + registerAppURL?: string; + deploymentToken?: string; +}; +/** Vercel APL + * + * Use environment variables for auth data storage. To update data on existing deployment, + * theres 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 it's limitations: + * - only stores single auth data entry (setting up a new one will overwrite previous values) + * - changing the environment variables require server restart + * + * With this APL we recommend 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]; + if (!registerAppURL) { + throw new Error("Misconfiguration: please provide registerAppUrl"); + } + const deploymentToken = + config?.deploymentToken || process.env[VercelAPLVariables.SALEOR_DEPLOYMENT_TOKEN]; + if (!deploymentToken) { + throw new Error("Misconfiguration: please provide deploymentToken"); + } + + this.registerAppURL = registerAppURL; + this.deploymentToken = deploymentToken; + } + + private async saveDataToVercel(authData?: AuthData) { + debug(`saveDataToVercel with: ${authData}`); + try { + await fetch(this.registerAppURL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + token: this.deploymentToken, + envs: { + [VercelAPLVariables.TOKEN_VARIABLE_NAME]: authData?.token || "", + [VercelAPLVariables.DOMAIN_VARIABLE_NAME]: authData?.domain || "", + }, + }), + }); + } catch (error) { + debug("Error during saving the data:", error); + throw new Error(`VercelAPL was not able to save auth data${error}`); + } + } + + async get(domain: string) { + const authData = getEnvAuth(); + + if (authData && domain === authData?.domain) { + return authData; + } + return undefined; + } + + async set(authData: AuthData) { + await this.saveDataToVercel(authData); + } + + async delete(domain: string) { + if (domain === getEnvAuth()?.domain) { + // Override existing data with the empty values + await this.saveDataToVercel(); + } + } + + async getAll() { + const authData = getEnvAuth(); + if (!authData) { + return []; + } + return [authData]; + } +} diff --git a/src/middleware/middleware-debug.ts b/src/middleware/middleware-debug.ts index 239954b..a44ea0e 100644 --- a/src/middleware/middleware-debug.ts +++ b/src/middleware/middleware-debug.ts @@ -5,7 +5,7 @@ import { createDebug } from "../debug"; export const createMiddlewareDebug = (middleware: string) => createDebug(`Middleware:${middleware}`); -type DebugFactory = (handlerName: string) => (msg: string, ...args: any[]) => void; +type DebugFactory = (handlerName: string) => (msg: string, ...args: unknown[]) => void; /** * Experimental. Needs to be tested and evaluated on security