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:
parent
f38f3f403b
commit
dfd632bb14
10 changed files with 293 additions and 3 deletions
5
.changeset/friendly-melons-approve.md
Normal file
5
.changeset/friendly-melons-approve.md
Normal 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.
|
5
.changeset/long-hornets-yawn.md
Normal file
5
.changeset/long-hornets-yawn.md
Normal 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
|
5
.changeset/strong-rings-help.md
Normal file
5
.changeset/strong-rings-help.md
Normal 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
|
69
docs/apl.md
69
docs/apl.md
|
@ -184,3 +184,72 @@ const apl = new UpstashAPL({
|
||||||
```
|
```
|
||||||
|
|
||||||
Or using environment variables: `UPSTASH_TOKEN`, `UPSTASH_URL`.
|
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
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"build": "tsup && clear-package-json package.json -o dist/package.json --fields publishConfig && pnpm copy-readme",
|
"build": "tsup && clear-package-json package.json -o dist/package.json --fields publishConfig && pnpm copy-readme",
|
||||||
"clean": "rm -rf ./dist/*",
|
"clean": "rm -rf ./dist/*",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ci": "CI=true vitest --coverage",
|
"test:ci": "CI=true vitest --coverage --silent",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"lint": "prettier --loglevel warn --write . && eslint --fix .",
|
"lint": "prettier --loglevel warn --write . && eslint --fix .",
|
||||||
"copy-readme": "cp README.md dist/README.md",
|
"copy-readme": "cp README.md dist/README.md",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
export interface AuthData {
|
export interface AuthData {
|
||||||
domain: string;
|
domain?: string;
|
||||||
token: string;
|
token: string;
|
||||||
saleorApiUrl: string;
|
saleorApiUrl: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
jwks: string;
|
jwks?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AplReadyResult =
|
export type AplReadyResult =
|
||||||
|
|
93
src/APL/env-apl.test.ts
Normal file
93
src/APL/env-apl.test.ts
Normal 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
108
src/APL/env-apl.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./apl";
|
export * from "./apl";
|
||||||
|
export * from "./env-apl";
|
||||||
export * from "./file-apl";
|
export * from "./file-apl";
|
||||||
export * from "./saleor-cloud-apl";
|
export * from "./saleor-cloud-apl";
|
||||||
export * from "./upstash-apl";
|
export * from "./upstash-apl";
|
||||||
|
|
|
@ -157,6 +157,10 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async <T>({
|
||||||
try {
|
try {
|
||||||
debug("Will verify signature with JWKS saved in AuthData");
|
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);
|
await verifySignatureWithJwks(authData.jwks, signature, rawBody);
|
||||||
} catch {
|
} catch {
|
||||||
debug("Request signature check failed. Refresh the JWKS cache and check again");
|
debug("Request signature check failed. Refresh the JWKS cache and check again");
|
||||||
|
|
Loading…
Reference in a new issue