Add EnvAPL (#213)

* Add EnvAPL

* Add extra check for api url matching

* Add EnvAPL in exported files

* Change Apl to APL

* Change configured to always true

* Add readme

* Replace console table with log
This commit is contained in:
Lukasz Ostrowski 2023-03-14 11:34:53 +01:00 committed by GitHub
parent f38f3f403b
commit dfd632bb14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 293 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": minor
---
Add EnvAPL which is a read-only single-tenant APL implementation. It can print AuthData from registration process, but the developer is responsible to configure ENV and pass data to the constructor.

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": patch
---
"domain" field in AuthData is no longer required. It will be set by registerHandler, but if missing, domain can be resolved from saleorApiUrl

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": patch
---
JWKS field in AuthData is no longer required. registerHandler will try to set it for cache purposes, but not every time it is possible. If JWKS is not found, registerHandler will fetch it

View file

@ -184,3 +184,72 @@ const apl = new UpstashAPL({
```
Or using environment variables: `UPSTASH_TOKEN`, `UPSTASH_URL`.
### EnvAPL
For very simple, static & single-tenant applications, you may want to just set everything once, usually in env variables and just leave it.
There is a simple APL for this use case - `EnvAPL`
> **Warning**
> Using this APL is highly discouraged in any production environment.
> It will break if app token is regenerated, it will not work with any flow related to updates of the app.
#### EnvAPL usage
##### 1. Configure app to use env apl
```tsx
import { EnvAPL } from "@saleor/app-sdk/APL";
const apl = new EnvAPL({
env: {
/**
* Map your env variables here. You dont have these values yet
*/
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
/**
* Set it to "true" - during app registration check you app logs. APL will print values you need
*/
printAuthDataOnRegister: true,
});
```
##### 2. Set env variables
After step 1, you should see your logs in similar way:
```
┌──────────────┬─────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────┼─────────────────────────────────────────────┤
│ saleorApiUrl │ 'https://my-saleor-instance.cloud/graphql/' │
│ appId │ 'app-id' │
│ token │ 'some-token' │
│ jwks │ '{}' │
│ domain │ 'my-saleor-instance.cloud' │
└──────────────┴─────────────────────────────────────────────┘
```
You need to se env variables in your provider, eg. in Vercel - you need appId, token and saleorApiUrl.
Ensure name of variables match your step 1 constructor options
##### 3. Turn off logging and redeploy
After env is set, you should disable printing it for security reasons
Change constructor to include
```
printAuthDataOnRegister: false
```
Then, redeploy the app. It should be configured and work for this specific Saleor instance
Keep in mind that
- If you promote environment, you need to update saleorApiUrl
- If you reinstall the app, you need to repeat the process to receive new token and ID

View file

@ -10,7 +10,7 @@
"build": "tsup && clear-package-json package.json -o dist/package.json --fields publishConfig && pnpm copy-readme",
"clean": "rm -rf ./dist/*",
"test": "vitest",
"test:ci": "CI=true vitest --coverage",
"test:ci": "CI=true vitest --coverage --silent",
"prepare": "husky install",
"lint": "prettier --loglevel warn --write . && eslint --fix .",
"copy-readme": "cp README.md dist/README.md",

View file

@ -1,9 +1,9 @@
export interface AuthData {
domain: string;
domain?: string;
token: string;
saleorApiUrl: string;
appId: string;
jwks: string;
jwks?: string;
}
export type AplReadyResult =

93
src/APL/env-apl.test.ts Normal file
View file

@ -0,0 +1,93 @@
import { describe, expect, it, vi } from "vitest";
import { AuthData } from "./apl";
import { EnvAPL } from "./env-apl";
const getMockEnvVars = () => ({
SALEOR_APP_TOKEN: "some-token",
SALEOR_APP_ID: "app-id",
SALEOR_API_URL: "https://my-saleor-instance.cloud/graphql/",
});
const getMockAuthData = (): AuthData => ({
saleorApiUrl: "https://my-saleor-instance.cloud/graphql/",
appId: "app-id",
token: "some-token",
jwks: "{}",
domain: "my-saleor-instance.cloud",
});
describe("EnvAPL", () => {
it("Constructs when values are provided in constructor", () => {
const envVars = getMockEnvVars();
expect(
new EnvAPL({
env: {
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
})
).toBeDefined();
});
it("Prints auth data from \"set\" method in stdout if printAuthDataOnRegister set to \"true\"", async () => {
const envVars = getMockEnvVars();
vi.spyOn(console, "log");
const mockAuthData = getMockAuthData();
await new EnvAPL({
env: {
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
printAuthDataOnRegister: true,
}).set(mockAuthData);
// eslint-disable-next-line no-console
return expect(console.log).toHaveBeenNthCalledWith(
2,
/**
* Assert stringified values for formatting
*/
`{
"saleorApiUrl": "https://my-saleor-instance.cloud/graphql/",
"appId": "app-id",
"token": "some-token",
"jwks": "{}",
"domain": "my-saleor-instance.cloud"
}`
);
});
it("Returns authData from constructor in get() and getAll()", async () => {
const envVars = getMockEnvVars();
const apl = new EnvAPL({
env: {
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
printAuthDataOnRegister: true,
});
expect(await apl.get(envVars.SALEOR_API_URL)).toEqual({
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
});
expect(await apl.getAll()).toEqual([
{
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
]);
});
});

108
src/APL/env-apl.ts Normal file
View file

@ -0,0 +1,108 @@
import { APL, AuthData } from "./apl";
import { createAPLDebug } from "./apl-debug";
const debug = createAPLDebug("EnvAPL");
type AuthDataRequired = Omit<AuthData, "jwks" | "domain">;
type Options = {
env: Record<keyof AuthDataRequired, string>;
/**
* Enable to log auth data to stdout.
* Do it once to save data in ENV and disable it later.
*/
printAuthDataOnRegister?: boolean;
};
export class EnvAPL implements APL {
private defaultOptions: Partial<Options> = {
printAuthDataOnRegister: false,
};
options: Options;
constructor(options: Options) {
if (!this.isAuthDataValid(options.env)) {
// eslint-disable-next-line no-console
console.warn(
"EnvAPL constructor not filled with valid AuthData config. Try to install the app with \"printAuthDataOnRegister\" enabled and check console logs"
);
}
this.options = {
...this.defaultOptions,
...options,
};
}
private isAuthDataValid(authData: AuthData): boolean {
const keysToValidateAgainst: Array<keyof AuthData> = ["appId", "saleorApiUrl", "token"];
return keysToValidateAgainst.every(
(key) => authData[key] && typeof authData[key] === "string" && authData[key]!.length > 0
);
}
async isReady() {
return this.isAuthDataValid(this.options.env)
? ({
ready: true,
} as const)
: {
ready: false,
error: new Error("Auth data not valid, check constructor and pass env variables"),
};
}
/**
* Always return its configured, because otherwise .set() will never be called
* so env can't be printed
*/
async isConfigured() {
return {
configured: true,
} as const;
}
async set(authData: AuthData) {
if (this.options.printAuthDataOnRegister) {
// eslint-disable-next-line no-console
console.log("Displaying registration values for the app. Use them to configure EnvAPL");
// eslint-disable-next-line no-console
console.log(JSON.stringify(authData, null, 2));
console.warn(
"🛑'printAuthDataOnRegister' option should be turned off once APL is configured, to avoid possible leaks"
);
}
debug("Called set method");
}
async get(saleorApiUrl: string) {
if (!this.isAuthDataValid(this.options.env)) {
debug("Trying to get AuthData but APL constructor was not filled with proper AuthData");
return undefined;
}
if (saleorApiUrl !== this.options.env.saleorApiUrl) {
throw new Error(
`Requested AuthData for domain "${saleorApiUrl}", however APL is configured for ${this.options.env.saleorApiUrl}. You may trying to install app in invalid Saleor URL `
);
}
return this.options.env;
}
async getAll() {
if (!this.isAuthDataValid(this.options.env)) {
return [];
}
const authData = await this.get(this.options.env.saleorApiUrl);
return authData ? [authData] : [];
}
async delete() {
debug("Called delete method");
}
}

View file

@ -1,4 +1,5 @@
export * from "./apl";
export * from "./env-apl";
export * from "./file-apl";
export * from "./saleor-cloud-apl";
export * from "./upstash-apl";

View file

@ -157,6 +157,10 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async <T>({
try {
debug("Will verify signature with JWKS saved in AuthData");
if (!authData.jwks) {
throw new Error("JWKS not found in AuthData");
}
await verifySignatureWithJwks(authData.jwks, signature, rawBody);
} catch {
debug("Request signature check failed. Refresh the JWKS cache and check again");