Skip to content

Commit

Permalink
port over refactors and fixes from #57
Browse files Browse the repository at this point in the history
refactors (lint, split endpoints from functions, etc)
make purchasing from multiple tabs harder
make the transaction token a signed JWT
retarget to es2018 to allow named regex capture groups
add small grace period for extension logs
prevent replaying bits transactions
detect mobile slightly differently
  • Loading branch information
Govorunb committed Jul 14, 2024
1 parent a9655a1 commit 7899c12
Show file tree
Hide file tree
Showing 44 changed files with 1,630 additions and 1,236 deletions.
55 changes: 48 additions & 7 deletions common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,57 @@ export type Config = {

export type Cart = {
version: number;
clientSession: string; // any string to disambiguate between multiple tabs
id: string;
sku: string;
args: { [name: string]: any };
};

export type IdentifiableCart = Cart & {
userId: string;
};
export type IdentifiableCart = Cart & { userId: string };

export type Transaction = {
receipt: string;
token: string;
token: string; // JWT with TransactionToken (given by EBS on prepurchase)
clientSession: string; // same session as in Cart
receipt: string; // JWT with BitsTransactionPayload (coming from Twitch)
};
export type DecodedTransaction = {
token: TransactionTokenPayload;
receipt: BitsTransactionPayload;
};

export type TransactionToken = {
id: string;
time: number; // Unix millis
user: {
id: string; // user channel id
};
product: {
sku: string;
cost: number;
};
};
export type TransactionTokenPayload = {
exp: number;
data: TransactionToken;
};

export type BitsTransactionPayload = {
topic: string;
exp: number;
data: {
transactionId: string;
time: string;
userId: string;
product: {
domainId: string;
sku: string;
displayName: string;
cost: {
amount: number;
type: "bits";
};
};
};
};

export type PubSubMessage = {
Expand Down Expand Up @@ -123,9 +162,11 @@ export type Order = {
id: string;
userId: string;
state: OrderState;
cart?: Cart;
cart: Cart;
receipt?: string;
result?: string;
createdAt: number;
updatedAt: number;
};
};

export type Callback<T> = (data: T) => void;
4 changes: 3 additions & 1 deletion ebs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ async function main() {
app.listen(port, () => {
console.log("Listening on port " + port);

// add endpoints
require("./modules/config");
require("./modules/orders");
require("./modules/game");
require("./modules/orders");
require("./modules/twitch");
require("./modules/user");

const { setIngame } = require("./modules/config");

Expand Down
48 changes: 48 additions & 0 deletions ebs/src/modules/config/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Webhooks } from "@octokit/webhooks";
import { getConfig, getRawConfigData, sendRefresh } from ".";
import { app } from "../..";
import { asyncCatch } from "../../util/middleware";

const webhooks = new Webhooks({
secret: process.env.PRIVATE_API_KEY!,
});

app.get(
"/public/config",
asyncCatch(async (req, res) => {
const config = await getConfig();
res.send(JSON.stringify(config));
})
);

app.post(
"/webhook/refresh",
asyncCatch(async (req, res) => {
// github webhook
const signature = req.headers["x-hub-signature-256"] as string;
const body = JSON.stringify(req.body);

if (!(await webhooks.verify(body, signature))) {
res.sendStatus(403);
return;
}

// only refresh if the config.json file was changed
if (req.body.commits.some((commit: any) => commit.modified.includes("config.json"))) {
sendRefresh();

res.status(200).send("Config refreshed.");
} else {
res.status(200).send("Config not refreshed.");
}
})
);

app.get(
"/private/refresh",
asyncCatch(async (_, res) => {
sendRefresh();

res.send(await getRawConfigData());
})
);
105 changes: 33 additions & 72 deletions ebs/src/modules/config.ts → ebs/src/modules/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { Config } from "common/types";
import { app } from "..";
import { sendPubSubMessage } from "../util/pubsub";
import { sendPubSubMessage } from "../../util/pubsub";
import { compressSync, strFromU8, strToU8 } from "fflate";
import { asyncCatch } from "../util/middleware";
import { Webhooks } from "@octokit/webhooks";
import { sendToLogger } from "../util/logger";
import { sendToLogger } from "../../util/logger";

let activeConfig: Config | undefined;
let configData: Config | undefined;
let activeConfig: Config | undefined;
let ingameState = false;

const apiURL = "https://api.github.com/repos/vedalai/swarm-control/contents/config.json";
const rawURL = "https://raw.githubusercontent.com/VedalAI/swarm-control/main/config.json";

require("./endpoints");

(async () => {
const config = await getConfig();
await broadcastConfigRefresh(config);
})().then();

async function fetchConfig(): Promise<Config> {
let url = `${apiURL}?${Date.now()}`;

try {
const response = await fetch(url);
const responseData = await response.json();

const data: Config = JSON.parse(atob(responseData.content))
const data: Config = JSON.parse(atob(responseData.content));

return data;
} catch (e: any) {
Expand All @@ -42,12 +47,12 @@ async function fetchConfig(): Promise<Config> {
url = `${rawURL}?${Date.now()}`;
const response = await fetch(url);
const data: Config = await response.json();

return data;
} catch (e: any) {
console.error("Error when fetching config from raw URL, panic");
console.error(e);

sendToLogger({
transactionToken: null,
userIdInsecure: null,
Expand All @@ -59,14 +64,23 @@ async function fetchConfig(): Promise<Config> {
},
],
}).then();

return {
version: -1,
message: "Error when fetching config from raw URL, panic",
};
}
}
}

export function isIngame() {
return ingameState;
}

export async function setIngame(newIngame: boolean) {
if (ingameState == newIngame) return;
ingameState = newIngame;
await setActiveConfig(await getRawConfigData());
}

function processConfig(data: Config) {
Expand All @@ -85,6 +99,14 @@ export async function getConfig(): Promise<Config> {
return activeConfig!;
}

export async function getRawConfigData(): Promise<Config> {
if (!configData) {
await refreshConfig();
}

return configData!;
}

export async function setActiveConfig(data: Config) {
activeConfig = processConfig(data);
await broadcastConfigRefresh(activeConfig);
Expand All @@ -97,74 +119,13 @@ export async function broadcastConfigRefresh(config: Config) {
});
}

let ingameState: boolean = false;

export function isIngame() {
return ingameState;
}

export function setIngame(newIngame: boolean) {
if (ingameState == newIngame) return;
ingameState = newIngame;
setActiveConfig(configData!).then();
}

async function refreshConfig() {
configData = await fetchConfig();
activeConfig = processConfig(configData);
}

app.get(
"/private/refresh",
asyncCatch(async (_, res) => {
sendRefresh();

res.send(configData);
})
);

const webhooks = new Webhooks({
secret: process.env.PRIVATE_API_KEY!,
});

app.post(
"/webhook/refresh",
asyncCatch(async (req, res) => {
// github webhook
const signature = req.headers["x-hub-signature-256"] as string;
const body = JSON.stringify(req.body);

if (!(await webhooks.verify(body, signature))) {
res.sendStatus(403);
return;
}

// only refresh if the config.json file was changed
if (req.body.commits.some((commit: any) => commit.modified.includes("config.json"))) {
sendRefresh();

res.status(200).send("Config refreshed.");
} else {
res.status(200).send("Config not refreshed.");
}
})
);

async function sendRefresh() {
export async function sendRefresh() {
await refreshConfig();
console.log("Refreshed config, new config version is ", activeConfig!.version);
await broadcastConfigRefresh(activeConfig!);
}

app.get(
"/public/config",
asyncCatch(async (req, res) => {
const config = await getConfig();
res.send(JSON.stringify(config));
})
);

(async () => {
const config = await getConfig();
await broadcastConfigRefresh(config);
})().then();
Loading

0 comments on commit 7899c12

Please sign in to comment.