Add APL for storing installation data

This commit is contained in:
Jonatan Witoszek 2023-08-18 13:44:07 +02:00
parent cd65676aa5
commit 2eea13fe81
No known key found for this signature in database
GPG key ID: D35056EA9CCE8472
8 changed files with 2138 additions and 13 deletions

51
deno-apl.ts Normal file
View file

@ -0,0 +1,51 @@
import {
APL,
AuthData,
AplReadyResult,
AplConfiguredResult,
} from "npm:@saleor/app-sdk@0.43.0/APL";
const kv = await Deno.openKv();
const KV_KEY = "authData";
export class DenoAPL implements APL {
async get(saleorApiUrl: string) {
const authData = await kv.get<AuthData>([KV_KEY, saleorApiUrl]);
if (authData.value) {
return authData.value;
}
}
async set(authData: AuthData) {
await kv.set([KV_KEY, authData.saleorApiUrl], authData);
}
async delete(saleorApiUrl: string) {
return await kv.delete([KV_KEY, saleorApiUrl]);
}
async getAll() {
const list = kv.list<AuthData>({ prefix: [KV_KEY] });
const authDataList: AuthData[] = [];
for await (const res of list) {
authDataList.push(res.value);
}
return authDataList;
}
async isReady(): Promise<AplReadyResult> {
return {
ready: true,
};
}
async isConfigured(): Promise<AplConfiguredResult> {
return {
configured: true,
};
}
}

View file

@ -1,6 +1,6 @@
{ {
"importMap": "./import_map.json", "importMap": "./import_map.json",
"tasks": { "tasks": {
"start": "deno run -A -r main.ts" "start": "deno run -A --unstable main.ts"
} }
} }

1928
deno.lock

File diff suppressed because it is too large Load diff

36
invariant.ts Normal file
View file

@ -0,0 +1,36 @@
// From https://github.com/alexreardon/tiny-invariant/blob/master/src/tiny-invariant.ts
const isDenoDeploy: boolean = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined;
const prefix: string = "Invariant failed";
// Throw an error if the condition fails
// Strip out error messages for production
// > Not providing an inline default argument for message as the result is smaller
export default function invariant(
condition: any,
// Can provide a string, or a function that returns a string for cases where
// the message takes a fair amount of effort to compute
message?: string | (() => string)
): asserts condition {
if (condition) {
return;
}
// Condition not passed
// In production we strip the message but still throw
if (isDenoDeploy) {
throw new Error(prefix);
}
// When not in production we allow the message to pass through
// *This block will be removed in production builds*
const provided: string | undefined =
typeof message === "function" ? message() : message;
// Options:
// 1. message provided: `${prefix}: ${provided}`
// 2. message not provided: prefix
const value: string = provided ? `${prefix}: ${provided}` : prefix;
throw new Error(value);
}

View file

@ -1,2 +1,2 @@
run: run:
deno run -A main.ts deno run -A --unstable main.ts

63
main.ts
View file

@ -1,3 +1,5 @@
/// <reference lib="deno.unstable" />
import { serve } from "wren/mod.ts"; import { serve } from "wren/mod.ts";
import { GET, POST } from "wren/route.ts"; import { GET, POST } from "wren/route.ts";
import * as Response from "wren/response.ts"; import * as Response from "wren/response.ts";
@ -10,6 +12,15 @@ import {
transactionInitialize, transactionInitialize,
transactionProcess, transactionProcess,
} from "./subscriptions.ts"; } from "./subscriptions.ts";
import {
SALEOR_DOMAIN_HEADER,
SALEOR_API_URL_HEADER,
} from "npm:@saleor/app-sdk@0.43.0/const";
import { DenoAPL } from "./deno-apl.ts";
import { fetchRemoteJwks, getAppId } from "./utils.ts";
import { AuthData } from "npm:@saleor/app-sdk@0.43.0/APL";
const apl = new DenoAPL();
interface ActionRequestResponse { interface ActionRequestResponse {
pspReference: string; pspReference: string;
@ -102,17 +113,61 @@ const routes = [
} satisfies AppManifest); } satisfies AppManifest);
}), }),
POST("/install", async (req) => { POST("/install", async (req) => {
console.log("install");
const json = await req.json(); const json = await req.json();
console.log("install", json); const authToken = json.auth_token;
const saleorDomain = req.headers.get(SALEOR_DOMAIN_HEADER);
const saleorApiUrl = req.headers.get(SALEOR_API_URL_HEADER);
if (!authToken || !saleorDomain || !saleorApiUrl) {
return Response.BadRequest({
code: "MISSING_HEADER",
message: "One of requried headers is missing",
});
}
const appId = await getAppId({ saleorApiUrl, token: authToken });
if (!appId) {
return Response.BadRequest({
code: "UNKNOWN_APP_ID",
message: `The auth data given during registration request could not be used to fetch app ID.
This usually means that App could not connect to Saleor during installation. Saleor URL that App tried to connect: ${saleorApiUrl}`,
});
}
const jwks = await fetchRemoteJwks(saleorApiUrl);
if (!jwks) {
return Response.BadRequest({
code: "JWKS_NOT_AVAILABLE",
message: "Can't fetch the remote JWKS.",
});
}
const authData: AuthData = {
domain: saleorDomain,
token: authToken,
saleorApiUrl,
appId,
jwks,
};
try {
apl.set(authData);
} catch (_e) {
return Response.InternalServerError({
code: "APL_SAVE_ERROR",
message: "Cannot save APL",
});
}
return Response.OK({ return Response.OK({
success: true, success: true,
}); });
}), }),
POST("/gateway-initialize", async (req: Request) => { POST("/gateway-initialize", async (req: Request) => {
const json = await req.json(); const json = await req.json();
console.log("gateway initialize", json); console.log("/gateway-initialize - json", json);
console.log("headers", req.headers); console.log("/gateway-initialize - headers", req.headers);
return Response.OK({ return Response.OK({
data: { data: {
some: "data", some: "data",

55
utils.ts Normal file
View file

@ -0,0 +1,55 @@
import { getJwksUrlFromSaleorApiUrl } from "npm:@saleor/app-sdk@0.43.0/urls";
export const fetchRemoteJwks = async (saleorApiUrl: string) => {
const jwksResponse = await fetch(getJwksUrlFromSaleorApiUrl(saleorApiUrl));
return jwksResponse.text();
};
type GetIdResponseType = {
data?: {
app?: {
id: string;
};
};
};
export interface GetAppIdProperties {
saleorApiUrl: string;
token: string;
}
export const getAppId = async ({
saleorApiUrl,
token,
}: GetAppIdProperties): Promise<string | undefined> => {
try {
const response = await fetch(saleorApiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: `
{
app{
id
}
}
`,
}),
});
if (response.status !== 200) {
console.error(
`Could not get the app ID: Saleor API has response code ${response.status}`
);
return undefined;
}
const body = (await response.json()) as GetIdResponseType;
const appId = body.data?.app?.id;
return appId;
} catch (e) {
console.error("Could not get the app ID: %O", e);
return undefined;
}
};