Add APL for storing installation data
This commit is contained in:
parent
cd65676aa5
commit
2eea13fe81
8 changed files with 2138 additions and 13 deletions
51
deno-apl.ts
Normal file
51
deno-apl.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
36
invariant.ts
Normal file
36
invariant.ts
Normal 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);
|
||||||
|
}
|
2
justfile
2
justfile
|
@ -1,2 +1,2 @@
|
||||||
run:
|
run:
|
||||||
deno run -A main.ts
|
deno run -A --unstable main.ts
|
||||||
|
|
63
main.ts
63
main.ts
|
@ -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
55
utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in a new issue